From aca5beaf6cc5a7f959c87758ff37b5cd83b2c0f2 Mon Sep 17 00:00:00 2001 From: mrhunsaker Date: Wed, 17 Dec 2025 12:47:34 -0700 Subject: [PATCH] javadoc updates --- .github/workflows/ci.yml | 24 + .github/workflows/package-linux.yml | 130 + .github/workflows/package-macos.yml | 70 + .github/workflows/package-windows.yml | 68 + .github/workflows/release.yml | 177 + .github/workflows/site-deploy.yml | 59 + .github/workflows/site.yml | 27 + .gitignore | 50 +- .vscode/settings.json | 4 +- CHANGELOG.md | 132 +- LICENSE | 402 +- META-INF/MANIFEST.MF | 12 +- README.md | 405 +- REPORT-pages-db-methods.md | 286 +- config/checkstyle/checkstyle.xml | 58 +- examples/.github/workflows/ci-actionlint.yml | 39 + examples/.github/workflows/ci-maven.yml | 30 + .../.github/workflows/deploy-javadocs.yml | 60 + .../.github/workflows/release-packages.yml | 107 + examples/.gitignore | 58 + examples/.gitmodules | 3 + examples/icons/scatter-plot-128.ico | Bin 0 -> 67646 bytes examples/icons/scatter-plot-128.png | Bin 0 -> 3600 bytes examples/icons/scatter-plot-16.ico | Bin 0 -> 1150 bytes examples/icons/scatter-plot-16.png | Bin 0 -> 430 bytes examples/icons/scatter-plot-24.ico | Bin 0 -> 2462 bytes examples/icons/scatter-plot-24.png | Bin 0 -> 822 bytes examples/icons/scatter-plot-256.ico | Bin 0 -> 270398 bytes examples/icons/scatter-plot-256.png | Bin 0 -> 7831 bytes examples/icons/scatter-plot-32.ico | Bin 0 -> 4286 bytes examples/icons/scatter-plot-32.png | Bin 0 -> 903 bytes examples/icons/scatter-plot-48.ico | Bin 0 -> 9662 bytes examples/icons/scatter-plot-48.png | Bin 0 -> 1995 bytes examples/icons/scatter-plot-512.ico | Bin 0 -> 1081406 bytes examples/icons/scatter-plot-512.png | Bin 0 -> 18656 bytes examples/icons/scatter-plot-64.ico | Bin 0 -> 16958 bytes examples/icons/scatter-plot-64.png | Bin 0 -> 1755 bytes examples/packaging/README.md | 176 + examples/packaging/appimage-builder.yml | 37 + examples/packaging/deb/postinst | 32 + examples/packaging/deb/prerm | 11 + examples/packaging/graph-digitizer.desktop | 11 + examples/packaging/rpm/graph-digitizer.spec | 49 + examples/pom.xml | 517 + examples/scripts/README.md | 76 + examples/scripts/archive-logs.ps1 | 45 + .../scripts/ci/build_runtime_in_container.sh | 35 + examples/scripts/create-mac-iconset.sh | 50 + examples/scripts/fix-md-lint.ps1 | 27 + examples/scripts/fix_md_lint.py | 32 + examples/scripts/fix_md_spacing.py | 40 + examples/scripts/generate-appimage.sh | 69 + examples/scripts/generate-deb.sh | 37 + examples/scripts/generate-dmg.sh | 37 + examples/scripts/generate-javadoc-index.sh | 280 + examples/scripts/generate-msi.ps1 | 86 + examples/scripts/generate-rpm.sh | 118 + examples/scripts/generate-snap.sh | 86 + examples/scripts/ingest-json-log.ps1 | 44 + examples/scripts/ingest_json_log.py | 49 + examples/scripts/select-icon.ps1 | 38 + examples/scripts/sign-macos.sh | 37 + examples/scripts/sign-windows.ps1 | 32 + examples/scripts/verify-msi.ps1 | 111 + json_Files/students.json.txt | 60 +- nbactions.xml | 40 +- pom.xml | 546 +- src/main/java/.gitkeep | 6 +- src/main/java/Abacus.java | 4 +- src/main/java/Braille.java | 6 +- src/main/java/BrailleNote.java | 4 +- src/main/java/BrailleSense.java | 4 +- src/main/java/CVI.java | 4 +- src/main/java/DigitalLiteracy.java | 4 +- src/main/java/IOS.java | 4 +- src/main/java/Keyboarding.java | 4 +- src/main/java/Main.java | 10 +- src/main/java/ScreenReader.java | 4 +- src/main/java/VersionUtil.java | 138 +- .../studentgui/app/DateChangeListener.java | 30 +- src/main/java/com/studentgui/app/Main.java | 1194 +- .../com/studentgui/app/PreferencesDialog.java | 154 +- .../app/SettingsChangeListener.java | 26 +- .../studentgui/app/StudentChangeListener.java | 24 +- .../com/studentgui/apphelpers/Database.java | 1130 +- .../com/studentgui/apphelpers/Helpers.java | 598 +- .../studentgui/apphelpers/PythonPlotter.java | 170 +- .../apphelpers/SessionJsonWriter.java | 250 +- .../com/studentgui/apphelpers/Settings.java | 114 +- .../studentgui/apphelpers/SqlGenerate.java | 380 +- .../com/studentgui/apphelpers/UiNotifier.java | 122 +- .../apphelpers/dto/AssessmentPayload.java | 82 +- .../apphelpers/dto/ContactPayload.java | 112 +- .../apphelpers/dto/KeyboardingPayload.java | 80 +- .../apphelpers/dto/NotesPayload.java | 56 +- .../apphelpers/dto/SessionPayload.java | 26 +- .../java/com/studentgui/apppages/Abacus.java | 851 +- .../java/com/studentgui/apppages/Braille.java | 871 +- .../com/studentgui/apppages/BrailleNote.java | 856 +- .../com/studentgui/apppages/BrailleSense.java | 654 +- .../java/com/studentgui/apppages/CVI.java | 562 +- .../com/studentgui/apppages/ContactLog.java | 446 +- .../studentgui/apppages/DigitalLiteracy.java | 844 +- .../com/studentgui/apppages/Homepage.java | 150 +- .../java/com/studentgui/apppages/IOS.java | 726 +- .../apppages/InstructionalMaterials.java | 134 +- .../com/studentgui/apppages/JLineGraph.java | 1643 +- .../com/studentgui/apppages/Keyboarding.java | 506 +- .../com/studentgui/apppages/Observations.java | 263 +- .../com/studentgui/apppages/ScreenReader.java | 795 +- .../com/studentgui/apppages/SessionNotes.java | 248 +- .../java/com/studentgui/apptheming/Theme.java | 834 +- .../com/studentgui/bootstrap/Bootstrap.java | 84 +- .../com/studentgui/test/BrailleSmokeTest.java | 44 +- .../com/studentgui/tools/GroupedSmoke.java | 154 +- .../tools/ProgrammaticPageSaveTest.java | 225 +- .../studentgui/tools/QueryStudentData.java | 193 +- .../tools/RenderStudentProgress.java | 176 +- .../java/com/studentgui/tools/SmokeTest.java | 140 +- .../studentgui/uicomp/PhaseScoreField.java | 542 +- src/main/resources/logback.xml | 100 +- src/main/resources/version.properties | 2 +- .../apphelpers/DatabaseContactLogTest.java | 50 +- .../apphelpers/SessionJsonWriterTest.java | 98 +- .../apphelpers/SqlGenerateTest.java | 56 +- .../JLineGraphDeterministicJitterTest.java | 84 +- .../studentgui/test/BrailleDatabaseTest.java | 96 +- .../com/studentgui/test/BrailleSmokeTest.java | 84 +- .../test/DatabaseEdgeCasesTest.java | 116 +- .../com/studentgui/test/DatabaseTest.java | 96 +- .../test/ExportBrailleReportsTest.java | 252 +- target/apidocs/legal/ADDITIONAL_LICENSE_INFO | 1 - target/apidocs/legal/ASSEMBLY_EXCEPTION | 1 - target/apidocs/legal/LICENSE | 1 - target/checkstyle-cachefile | 9 + target/checkstyle-checker.xml | 198 + target/checkstyle-result.xml | 3197 +++ target/classes/.gitkeep | 3 - target/classes/logback.xml | 100 +- target/classes/version.properties | 2 +- .../javadoc-options-javadoc-resources.xml | 5 +- target/maven-archiver/pom.properties | 3 - target/maven-javadoc-plugin-stale-data.txt | 112 +- .../compile/default-compile/createdFiles.lst | 115 +- .../compile/default-compile/inputFiles.lst | 106 +- .../default-testCompile/createdFiles.lst | 18 +- .../default-testCompile/inputFiles.lst | 18 +- target/{ => site}/apidocs/VersionUtil.html | 8 +- .../{ => site}/apidocs/allclasses-index.html | 49 +- .../{ => site}/apidocs/allpackages-index.html | 4 +- .../apidocs/class-use/VersionUtil.html | 4 +- .../studentgui/app/DateChangeListener.html | 8 +- .../apidocs/com/studentgui/app/Main.html | 32 +- .../com/studentgui/app/PreferencesDialog.html | 8 +- .../app/SettingsChangeListener.html | 8 +- .../studentgui/app/StudentChangeListener.html | 8 +- .../app/class-use/DateChangeListener.html | 16 +- .../com/studentgui/app/class-use/Main.html | 4 +- .../app/class-use/PreferencesDialog.html | 4 +- .../app/class-use/SettingsChangeListener.html | 7 +- .../app/class-use/StudentChangeListener.html | 16 +- .../com/studentgui/app/package-summary.html | 4 +- .../com/studentgui/app/package-tree.html | 4 +- .../com/studentgui/app/package-use.html | 4 +- .../apphelpers/Database.ResultsWithDates.html | 12 +- .../com/studentgui/apphelpers/Database.html | 30 +- .../com/studentgui/apphelpers/Helpers.html | 36 +- .../studentgui/apphelpers/PythonPlotter.html | 8 +- .../apphelpers/SessionJsonWriter.html | 14 +- .../com/studentgui/apphelpers/Settings.html | 10 +- .../studentgui/apphelpers/SqlGenerate.html | 8 +- .../com/studentgui/apphelpers/UiNotifier.html | 8 +- .../class-use/Database.ResultsWithDates.html | 4 +- .../apphelpers/class-use/Database.html | 4 +- .../apphelpers/class-use/Helpers.html | 4 +- .../apphelpers/class-use/PythonPlotter.html | 4 +- .../class-use/SessionJsonWriter.html | 4 +- .../apphelpers/class-use/Settings.html | 4 +- .../apphelpers/class-use/SqlGenerate.html | 4 +- .../apphelpers/class-use/UiNotifier.html | 4 +- .../apphelpers/dto/AssessmentPayload.html | 20 +- .../apphelpers/dto/ContactPayload.html | 30 +- .../apphelpers/dto/KeyboardingPayload.html | 22 +- .../apphelpers/dto/NotesPayload.html | 16 +- .../apphelpers/dto/SessionPayload.html | 8 +- .../dto/class-use/AssessmentPayload.html | 4 +- .../dto/class-use/ContactPayload.html | 4 +- .../dto/class-use/KeyboardingPayload.html | 4 +- .../dto/class-use/NotesPayload.html | 4 +- .../dto/class-use/SessionPayload.html | 4 +- .../apphelpers/dto/package-summary.html | 4 +- .../apphelpers/dto/package-tree.html | 4 +- .../apphelpers/dto/package-use.html | 4 +- .../apphelpers/package-summary.html | 4 +- .../studentgui/apphelpers/package-tree.html | 4 +- .../studentgui/apphelpers/package-use.html | 4 +- .../com/studentgui/apppages/Abacus.html | 58 +- .../com/studentgui/apppages/Braille.html | 56 +- .../com/studentgui/apppages/BrailleNote.html | 85 +- .../com/studentgui/apppages/BrailleSense.html | 57 +- .../apidocs/com/studentgui/apppages/CVI.html | 61 +- .../com/studentgui/apppages/ContactLog.html | 55 +- .../studentgui/apppages/DigitalLiteracy.html | 93 +- .../com/studentgui/apppages/Homepage.html | 8 +- .../apidocs/com/studentgui/apppages/IOS.html | 83 +- .../apppages/InstructionalMaterials.html | 38 +- .../com/studentgui/apppages/JLineGraph.html | 126 +- .../com/studentgui/apppages/Keyboarding.html | 49 +- .../com/studentgui/apppages/Observations.html | 38 +- .../com/studentgui/apppages/ScreenReader.html | 84 +- .../com/studentgui/apppages/SessionNotes.html | 39 +- .../studentgui/apppages/class-use/Abacus.html | 4 +- .../apppages/class-use/Braille.html | 4 +- .../apppages/class-use/BrailleNote.html | 4 +- .../apppages/class-use/BrailleSense.html | 4 +- .../studentgui/apppages/class-use/CVI.html | 4 +- .../apppages/class-use/ContactLog.html | 4 +- .../apppages/class-use/DigitalLiteracy.html | 4 +- .../apppages/class-use/Homepage.html | 4 +- .../studentgui/apppages/class-use/IOS.html | 4 +- .../class-use/InstructionalMaterials.html | 4 +- .../apppages/class-use/JLineGraph.html | 4 +- .../apppages/class-use/Keyboarding.html | 4 +- .../apppages/class-use/Observations.html | 4 +- .../apppages/class-use/ScreenReader.html | 4 +- .../apppages/class-use/SessionNotes.html | 4 +- .../studentgui/apppages/package-summary.html | 33 +- .../com/studentgui/apppages/package-tree.html | 4 +- .../com/studentgui/apppages/package-use.html | 7 +- .../com/studentgui/apptheming/Theme.html | 65 +- .../apptheming/class-use/Theme.html | 4 +- .../apptheming/package-summary.html | 6 +- .../studentgui/apptheming/package-tree.html | 4 +- .../studentgui/apptheming/package-use.html | 4 +- .../com/studentgui/bootstrap/Bootstrap.html | 8 +- .../bootstrap/class-use/Bootstrap.html | 4 +- .../studentgui/bootstrap/package-summary.html | 4 +- .../studentgui/bootstrap/package-tree.html | 4 +- .../com/studentgui/bootstrap/package-use.html | 4 +- .../com/studentgui/test/BrailleSmokeTest.html | 6 +- .../test/class-use/BrailleSmokeTest.html | 4 +- .../com/studentgui/test/package-summary.html | 4 +- .../com/studentgui/test/package-tree.html | 4 +- .../com/studentgui/test/package-use.html | 4 +- .../com/studentgui/tools/GroupedSmoke.html | 62 +- .../tools/ProgrammaticPageSaveTest.html | 69 +- .../studentgui/tools/QueryStudentData.html | 59 +- .../tools/RenderStudentProgress.html | 50 +- .../com/studentgui/tools/SmokeTest.html | 54 +- .../tools/class-use/GroupedSmoke.html | 4 +- .../class-use/ProgrammaticPageSaveTest.html | 4 +- .../tools/class-use/QueryStudentData.html | 4 +- .../class-use/RenderStudentProgress.html | 4 +- .../studentgui/tools/class-use/SmokeTest.html | 4 +- .../com/studentgui/tools/package-summary.html | 18 +- .../com/studentgui/tools/package-tree.html | 4 +- .../com/studentgui/tools/package-use.html | 4 +- .../studentgui/uicomp/PhaseScoreField.html | 30 +- .../uicomp/class-use/PhaseScoreField.html | 4 +- .../studentgui/uicomp/package-summary.html | 4 +- .../com/studentgui/uicomp/package-tree.html | 4 +- .../com/studentgui/uicomp/package-use.html | 4 +- target/{ => site}/apidocs/copy.svg | 0 .../{ => site}/apidocs/deprecated-list.html | 4 +- target/{ => site}/apidocs/element-list | 0 target/{ => site}/apidocs/help-doc.html | 4 +- target/{ => site}/apidocs/index-all.html | 49 +- target/{ => site}/apidocs/index.html | 4 +- .../apidocs/legal/ADDITIONAL_LICENSE_INFO | 37 + target/site/apidocs/legal/ASSEMBLY_EXCEPTION | 27 + target/site/apidocs/legal/LICENSE | 347 + target/{ => site}/apidocs/legal/jquery.md | 0 target/{ => site}/apidocs/legal/jqueryUI.md | 0 target/{ => site}/apidocs/link.svg | 0 .../{ => site}/apidocs/member-search-index.js | 0 .../{ => site}/apidocs/module-search-index.js | 0 .../{ => site}/apidocs/overview-summary.html | 4 +- target/{ => site}/apidocs/overview-tree.html | 4 +- .../apidocs/package-search-index.js | 0 .../{ => site}/apidocs/package-summary.html | 4 +- target/{ => site}/apidocs/package-tree.html | 4 +- target/{ => site}/apidocs/package-use.html | 4 +- target/{ => site}/apidocs/resources/glass.png | Bin target/{ => site}/apidocs/resources/x.png | Bin .../apidocs/script-dir/jquery-3.7.1.min.js | 0 .../apidocs/script-dir/jquery-ui.min.css | 0 .../apidocs/script-dir/jquery-ui.min.js | 0 target/{ => site}/apidocs/script.js | 0 target/{ => site}/apidocs/search-page.js | 0 target/{ => site}/apidocs/search.html | 4 +- target/{ => site}/apidocs/search.js | 0 .../{ => site}/apidocs/serialized-form.html | 4 +- target/site/apidocs/src-html/VersionUtil.html | 147 + .../studentgui/app/DateChangeListener.html | 93 + .../src-html/com/studentgui/app/Main.html | 675 + .../com/studentgui/app/PreferencesDialog.html | 155 + .../app/SettingsChangeListener.html | 91 + .../studentgui/app/StudentChangeListener.html | 90 + .../apphelpers/Database.ResultsWithDates.html | 643 + .../com/studentgui/apphelpers/Database.html | 643 + .../com/studentgui/apphelpers/Helpers.html | 377 + .../studentgui/apphelpers/PythonPlotter.html | 163 + .../apphelpers/SessionJsonWriter.html | 203 + .../com/studentgui/apphelpers/Settings.html | 135 + .../studentgui/apphelpers/SqlGenerate.html | 268 + .../com/studentgui/apphelpers/UiNotifier.html | 139 + .../apphelpers/dto/AssessmentPayload.html | 119 + .../apphelpers/dto/ContactPayload.html | 134 + .../apphelpers/dto/KeyboardingPayload.html | 118 + .../apphelpers/dto/NotesPayload.html | 106 + .../apphelpers/dto/SessionPayload.html | 91 + .../com/studentgui/apppages/Abacus.html | 520 + .../com/studentgui/apppages/Braille.html | 533 + .../com/studentgui/apppages/BrailleNote.html | 536 + .../com/studentgui/apppages/BrailleSense.html | 424 + .../src-html/com/studentgui/apppages/CVI.html | 380 + .../com/studentgui/apppages/ContactLog.html | 319 + .../studentgui/apppages/DigitalLiteracy.html | 535 + .../com/studentgui/apppages/Homepage.html | 153 + .../src-html/com/studentgui/apppages/IOS.html | 473 + .../apppages/InstructionalMaterials.html | 155 + .../com/studentgui/apppages/JLineGraph.html | 924 + .../com/studentgui/apppages/Keyboarding.html | 346 + .../com/studentgui/apppages/Observations.html | 223 + .../com/studentgui/apppages/ScreenReader.html | 507 + .../com/studentgui/apppages/SessionNotes.html | 216 + .../com/studentgui/apptheming/Theme.html | 517 + .../com/studentgui/bootstrap/Bootstrap.html | 120 + .../com/studentgui/test/BrailleSmokeTest.html | 100 + .../com/studentgui/tools/GroupedSmoke.html | 175 + .../tools/ProgrammaticPageSaveTest.html | 211 + .../studentgui/tools/QueryStudentData.html | 193 + .../tools/RenderStudentProgress.html | 180 + .../com/studentgui/tools/SmokeTest.html | 163 + .../studentgui/uicomp/PhaseScoreField.html | 349 + target/{ => site}/apidocs/stylesheet.css | 0 target/{ => site}/apidocs/tag-search-index.js | 0 .../{ => site}/apidocs/type-search-index.js | 0 target/site/checkstyle.html | 19470 ++++++++++++++++ target/site/css/maven-base.css | 168 + target/site/css/maven-theme.css | 161 + target/site/css/print.css | 26 + target/site/css/site.css | 1 + target/site/dependencies.html | 638 + target/site/dependency-info.html | 100 + target/site/images/close.gif | Bin 0 -> 279 bytes target/site/images/collapsed.gif | Bin 0 -> 53 bytes target/site/images/expanded.gif | Bin 0 -> 52 bytes target/site/images/external.png | Bin 0 -> 230 bytes target/site/images/icon_error_sml.gif | Bin 0 -> 1010 bytes target/site/images/icon_info_sml.gif | Bin 0 -> 606 bytes target/site/images/icon_success_sml.gif | Bin 0 -> 990 bytes target/site/images/icon_warning_sml.gif | Bin 0 -> 576 bytes .../images/logos/build-by-maven-black.png | Bin 0 -> 2294 bytes .../images/logos/build-by-maven-white.png | Bin 0 -> 2260 bytes target/site/images/logos/maven-feather.png | Bin 0 -> 3330 bytes target/site/images/newwindow.png | Bin 0 -> 220 bytes target/site/index.html | 75 + target/site/licenses.html | 282 + target/site/plugins.html | 149 + target/site/project-info.html | 98 + target/site/project-reports.html | 90 + target/site/summary.html | 119 + target/site/surefire.html | 323 + target/site/testapidocs/allclasses-index.html | 103 + .../site/testapidocs/allpackages-index.html | 73 + .../apphelpers/DatabaseContactLogTest.html | 183 + .../apphelpers/SessionJsonWriterTest.html | 184 + .../apphelpers/SqlGenerateTest.html | 183 + .../class-use/DatabaseContactLogTest.html | 62 + .../class-use/SessionJsonWriterTest.html | 62 + .../apphelpers/class-use/SqlGenerateTest.html | 62 + .../apphelpers/package-summary.html | 102 + .../studentgui/apphelpers/package-tree.html | 78 + .../studentgui/apphelpers/package-use.html | 62 + .../JLineGraphDeterministicJitterTest.html | 187 + .../JLineGraphDeterministicJitterTest.html | 62 + .../studentgui/apppages/package-summary.html | 98 + .../com/studentgui/apppages/package-tree.html | 76 + .../com/studentgui/apppages/package-use.html | 62 + .../studentgui/test/BrailleDatabaseTest.html | 186 + .../com/studentgui/test/BrailleSmokeTest.html | 186 + .../test/DatabaseEdgeCasesTest.html | 228 + .../com/studentgui/test/DatabaseTest.html | 201 + .../test/ExportBrailleReportsTest.html | 187 + .../test/class-use/BrailleDatabaseTest.html | 62 + .../test/class-use/BrailleSmokeTest.html | 62 + .../test/class-use/DatabaseEdgeCasesTest.html | 62 + .../test/class-use/DatabaseTest.html | 62 + .../class-use/ExportBrailleReportsTest.html | 62 + .../com/studentgui/test/package-summary.html | 116 + .../com/studentgui/test/package-tree.html | 80 + .../com/studentgui/test/package-use.html | 62 + target/site/testapidocs/copy.svg | 33 + target/site/testapidocs/element-list | 3 + target/site/testapidocs/help-doc.html | 193 + target/site/testapidocs/index-all.html | 177 + target/site/testapidocs/index.html | 75 + .../testapidocs/legal/ADDITIONAL_LICENSE_INFO | 37 + .../site/testapidocs/legal/ASSEMBLY_EXCEPTION | 27 + target/site/testapidocs/legal/LICENSE | 347 + target/site/testapidocs/legal/jquery.md | 26 + target/site/testapidocs/legal/jqueryUI.md | 49 + target/site/testapidocs/link.svg | 31 + .../site/testapidocs/member-search-index.js | 1 + .../site/testapidocs/module-search-index.js | 1 + target/site/testapidocs/overview-summary.html | 26 + target/site/testapidocs/overview-tree.html | 86 + .../site/testapidocs/package-search-index.js | 1 + target/site/testapidocs/resources/glass.png | Bin 0 -> 499 bytes target/site/testapidocs/resources/x.png | Bin 0 -> 394 bytes .../script-dir/jquery-3.7.1.min.js | 2 + .../testapidocs/script-dir/jquery-ui.min.css | 6 + .../testapidocs/script-dir/jquery-ui.min.js | 6 + target/site/testapidocs/script.js | 253 + target/site/testapidocs/search-page.js | 284 + target/site/testapidocs/search.html | 77 + target/site/testapidocs/search.js | 458 + .../apphelpers/DatabaseContactLogTest.html | 103 + .../apphelpers/SessionJsonWriterTest.html | 127 + .../apphelpers/SqlGenerateTest.html | 106 + .../JLineGraphDeterministicJitterTest.html | 120 + .../studentgui/test/BrailleDatabaseTest.html | 126 + .../com/studentgui/test/BrailleSmokeTest.html | 120 + .../test/DatabaseEdgeCasesTest.html | 136 + .../com/studentgui/test/DatabaseTest.html | 126 + .../test/ExportBrailleReportsTest.html | 204 + target/site/testapidocs/stylesheet.css | 1272 + target/site/testapidocs/tag-search-index.js | 1 + target/site/testapidocs/type-search-index.js | 1 + .../2025-10-29T13-28-26_658.dumpstream | 5 - ...tgui.apphelpers.DatabaseContactLogTest.xml | 101 +- ...ntgui.apphelpers.SessionJsonWriterTest.xml | 100 +- ....studentgui.apphelpers.SqlGenerateTest.xml | 101 +- ...ages.JLineGraphDeterministicJitterTest.xml | 99 +- ...om.studentgui.test.BrailleDatabaseTest.xml | 103 +- ...T-com.studentgui.test.BrailleSmokeTest.xml | 104 +- ....studentgui.test.DatabaseEdgeCasesTest.xml | 107 +- .../TEST-com.studentgui.test.DatabaseTest.xml | 103 +- ...udentgui.test.ExportBrailleReportsTest.xml | 125 +- ...tgui.apphelpers.DatabaseContactLogTest.txt | 2 +- ...ntgui.apphelpers.SessionJsonWriterTest.txt | 2 +- ....studentgui.apphelpers.SqlGenerateTest.txt | 2 +- ...ages.JLineGraphDeterministicJitterTest.txt | 2 +- ...om.studentgui.test.BrailleDatabaseTest.txt | 2 +- .../com.studentgui.test.BrailleSmokeTest.txt | 2 +- ....studentgui.test.DatabaseEdgeCasesTest.txt | 2 +- .../com.studentgui.test.DatabaseTest.txt | 2 +- ...udentgui.test.ExportBrailleReportsTest.txt | 2 +- 449 files changed, 61378 insertions(+), 10914 deletions(-) create mode 100644 .github/workflows/ci.yml create mode 100644 .github/workflows/package-linux.yml create mode 100644 .github/workflows/package-macos.yml create mode 100644 .github/workflows/package-windows.yml create mode 100644 .github/workflows/release.yml create mode 100644 .github/workflows/site-deploy.yml create mode 100644 .github/workflows/site.yml create mode 100644 examples/.github/workflows/ci-actionlint.yml create mode 100644 examples/.github/workflows/ci-maven.yml create mode 100644 examples/.github/workflows/deploy-javadocs.yml create mode 100644 examples/.github/workflows/release-packages.yml create mode 100644 examples/.gitignore create mode 100644 examples/.gitmodules create mode 100644 examples/icons/scatter-plot-128.ico create mode 100644 examples/icons/scatter-plot-128.png create mode 100644 examples/icons/scatter-plot-16.ico create mode 100644 examples/icons/scatter-plot-16.png create mode 100644 examples/icons/scatter-plot-24.ico create mode 100644 examples/icons/scatter-plot-24.png create mode 100644 examples/icons/scatter-plot-256.ico create mode 100644 examples/icons/scatter-plot-256.png create mode 100644 examples/icons/scatter-plot-32.ico create mode 100644 examples/icons/scatter-plot-32.png create mode 100644 examples/icons/scatter-plot-48.ico create mode 100644 examples/icons/scatter-plot-48.png create mode 100644 examples/icons/scatter-plot-512.ico create mode 100644 examples/icons/scatter-plot-512.png create mode 100644 examples/icons/scatter-plot-64.ico create mode 100644 examples/icons/scatter-plot-64.png create mode 100644 examples/packaging/README.md create mode 100644 examples/packaging/appimage-builder.yml create mode 100644 examples/packaging/deb/postinst create mode 100644 examples/packaging/deb/prerm create mode 100644 examples/packaging/graph-digitizer.desktop create mode 100644 examples/packaging/rpm/graph-digitizer.spec create mode 100644 examples/pom.xml create mode 100644 examples/scripts/README.md create mode 100644 examples/scripts/archive-logs.ps1 create mode 100644 examples/scripts/ci/build_runtime_in_container.sh create mode 100644 examples/scripts/create-mac-iconset.sh create mode 100644 examples/scripts/fix-md-lint.ps1 create mode 100644 examples/scripts/fix_md_lint.py create mode 100644 examples/scripts/fix_md_spacing.py create mode 100644 examples/scripts/generate-appimage.sh create mode 100644 examples/scripts/generate-deb.sh create mode 100644 examples/scripts/generate-dmg.sh create mode 100644 examples/scripts/generate-javadoc-index.sh create mode 100644 examples/scripts/generate-msi.ps1 create mode 100644 examples/scripts/generate-rpm.sh create mode 100644 examples/scripts/generate-snap.sh create mode 100644 examples/scripts/ingest-json-log.ps1 create mode 100644 examples/scripts/ingest_json_log.py create mode 100644 examples/scripts/select-icon.ps1 create mode 100644 examples/scripts/sign-macos.sh create mode 100644 examples/scripts/sign-windows.ps1 create mode 100644 examples/scripts/verify-msi.ps1 delete mode 100644 target/apidocs/legal/ADDITIONAL_LICENSE_INFO delete mode 100644 target/apidocs/legal/ASSEMBLY_EXCEPTION delete mode 100644 target/apidocs/legal/LICENSE create mode 100644 target/checkstyle-cachefile create mode 100644 target/checkstyle-checker.xml create mode 100644 target/checkstyle-result.xml delete mode 100644 target/classes/.gitkeep delete mode 100644 target/maven-archiver/pom.properties rename target/{ => site}/apidocs/VersionUtil.html (96%) rename target/{ => site}/apidocs/allclasses-index.html (90%) rename target/{ => site}/apidocs/allpackages-index.html (97%) rename target/{ => site}/apidocs/class-use/VersionUtil.html (95%) rename target/{ => site}/apidocs/com/studentgui/app/DateChangeListener.html (92%) rename target/{ => site}/apidocs/com/studentgui/app/Main.html (87%) rename target/{ => site}/apidocs/com/studentgui/app/PreferencesDialog.html (94%) rename target/{ => site}/apidocs/com/studentgui/app/SettingsChangeListener.html (94%) rename target/{ => site}/apidocs/com/studentgui/app/StudentChangeListener.html (92%) rename target/{ => site}/apidocs/com/studentgui/app/class-use/DateChangeListener.html (93%) rename target/{ => site}/apidocs/com/studentgui/app/class-use/Main.html (96%) rename target/{ => site}/apidocs/com/studentgui/app/class-use/PreferencesDialog.html (96%) rename target/{ => site}/apidocs/com/studentgui/app/class-use/SettingsChangeListener.html (96%) rename target/{ => site}/apidocs/com/studentgui/app/class-use/StudentChangeListener.html (93%) rename target/{ => site}/apidocs/com/studentgui/app/package-summary.html (98%) rename target/{ => site}/apidocs/com/studentgui/app/package-tree.html (97%) rename target/{ => site}/apidocs/com/studentgui/app/package-use.html (98%) rename target/{ => site}/apidocs/com/studentgui/apphelpers/Database.ResultsWithDates.html (92%) rename target/{ => site}/apidocs/com/studentgui/apphelpers/Database.html (92%) rename target/{ => site}/apidocs/com/studentgui/apphelpers/Helpers.html (89%) rename target/{ => site}/apidocs/com/studentgui/apphelpers/PythonPlotter.html (95%) rename target/{ => site}/apidocs/com/studentgui/apphelpers/SessionJsonWriter.html (91%) rename target/{ => site}/apidocs/com/studentgui/apphelpers/Settings.html (92%) rename target/{ => site}/apidocs/com/studentgui/apphelpers/SqlGenerate.html (96%) rename target/{ => site}/apidocs/com/studentgui/apphelpers/UiNotifier.html (94%) rename target/{ => site}/apidocs/com/studentgui/apphelpers/class-use/Database.ResultsWithDates.html (97%) rename target/{ => site}/apidocs/com/studentgui/apphelpers/class-use/Database.html (96%) rename target/{ => site}/apidocs/com/studentgui/apphelpers/class-use/Helpers.html (96%) rename target/{ => site}/apidocs/com/studentgui/apphelpers/class-use/PythonPlotter.html (96%) rename target/{ => site}/apidocs/com/studentgui/apphelpers/class-use/SessionJsonWriter.html (96%) rename target/{ => site}/apidocs/com/studentgui/apphelpers/class-use/Settings.html (96%) rename target/{ => site}/apidocs/com/studentgui/apphelpers/class-use/SqlGenerate.html (96%) rename target/{ => site}/apidocs/com/studentgui/apphelpers/class-use/UiNotifier.html (96%) rename target/{ => site}/apidocs/com/studentgui/apphelpers/dto/AssessmentPayload.html (92%) rename target/{ => site}/apidocs/com/studentgui/apphelpers/dto/ContactPayload.html (91%) rename target/{ => site}/apidocs/com/studentgui/apphelpers/dto/KeyboardingPayload.html (91%) rename target/{ => site}/apidocs/com/studentgui/apphelpers/dto/NotesPayload.html (93%) rename target/{ => site}/apidocs/com/studentgui/apphelpers/dto/SessionPayload.html (94%) rename target/{ => site}/apidocs/com/studentgui/apphelpers/dto/class-use/AssessmentPayload.html (96%) rename target/{ => site}/apidocs/com/studentgui/apphelpers/dto/class-use/ContactPayload.html (97%) rename target/{ => site}/apidocs/com/studentgui/apphelpers/dto/class-use/KeyboardingPayload.html (96%) rename target/{ => site}/apidocs/com/studentgui/apphelpers/dto/class-use/NotesPayload.html (96%) rename target/{ => site}/apidocs/com/studentgui/apphelpers/dto/class-use/SessionPayload.html (98%) rename target/{ => site}/apidocs/com/studentgui/apphelpers/dto/package-summary.html (98%) rename target/{ => site}/apidocs/com/studentgui/apphelpers/dto/package-tree.html (97%) rename target/{ => site}/apidocs/com/studentgui/apphelpers/dto/package-use.html (97%) rename target/{ => site}/apidocs/com/studentgui/apphelpers/package-summary.html (98%) rename target/{ => site}/apidocs/com/studentgui/apphelpers/package-tree.html (97%) rename target/{ => site}/apidocs/com/studentgui/apphelpers/package-use.html (97%) rename target/{ => site}/apidocs/com/studentgui/apppages/Abacus.html (95%) rename target/{ => site}/apidocs/com/studentgui/apppages/Braille.html (95%) rename target/{ => site}/apidocs/com/studentgui/apppages/BrailleNote.html (94%) rename target/{ => site}/apidocs/com/studentgui/apppages/BrailleSense.html (95%) rename target/{ => site}/apidocs/com/studentgui/apppages/CVI.html (96%) rename target/{ => site}/apidocs/com/studentgui/apppages/ContactLog.html (95%) rename target/{ => site}/apidocs/com/studentgui/apppages/DigitalLiteracy.html (94%) rename target/{ => site}/apidocs/com/studentgui/apppages/Homepage.html (96%) rename target/{ => site}/apidocs/com/studentgui/apppages/IOS.html (95%) rename target/{ => site}/apidocs/com/studentgui/apppages/InstructionalMaterials.html (98%) rename target/{ => site}/apidocs/com/studentgui/apppages/JLineGraph.html (91%) rename target/{ => site}/apidocs/com/studentgui/apppages/Keyboarding.html (96%) rename target/{ => site}/apidocs/com/studentgui/apppages/Observations.html (97%) rename target/{ => site}/apidocs/com/studentgui/apppages/ScreenReader.html (94%) rename target/{ => site}/apidocs/com/studentgui/apppages/SessionNotes.html (97%) rename target/{ => site}/apidocs/com/studentgui/apppages/class-use/Abacus.html (96%) rename target/{ => site}/apidocs/com/studentgui/apppages/class-use/Braille.html (96%) rename target/{ => site}/apidocs/com/studentgui/apppages/class-use/BrailleNote.html (96%) rename target/{ => site}/apidocs/com/studentgui/apppages/class-use/BrailleSense.html (96%) rename target/{ => site}/apidocs/com/studentgui/apppages/class-use/CVI.html (96%) rename target/{ => site}/apidocs/com/studentgui/apppages/class-use/ContactLog.html (96%) rename target/{ => site}/apidocs/com/studentgui/apppages/class-use/DigitalLiteracy.html (96%) rename target/{ => site}/apidocs/com/studentgui/apppages/class-use/Homepage.html (96%) rename target/{ => site}/apidocs/com/studentgui/apppages/class-use/IOS.html (96%) rename target/{ => site}/apidocs/com/studentgui/apppages/class-use/InstructionalMaterials.html (96%) rename target/{ => site}/apidocs/com/studentgui/apppages/class-use/JLineGraph.html (99%) rename target/{ => site}/apidocs/com/studentgui/apppages/class-use/Keyboarding.html (96%) rename target/{ => site}/apidocs/com/studentgui/apppages/class-use/Observations.html (96%) rename target/{ => site}/apidocs/com/studentgui/apppages/class-use/ScreenReader.html (96%) rename target/{ => site}/apidocs/com/studentgui/apppages/class-use/SessionNotes.html (96%) rename target/{ => site}/apidocs/com/studentgui/apppages/package-summary.html (84%) rename target/{ => site}/apidocs/com/studentgui/apppages/package-tree.html (98%) rename target/{ => site}/apidocs/com/studentgui/apppages/package-use.html (94%) rename target/{ => site}/apidocs/com/studentgui/apptheming/Theme.html (72%) rename target/{ => site}/apidocs/com/studentgui/apptheming/class-use/Theme.html (96%) rename target/{ => site}/apidocs/com/studentgui/apptheming/package-summary.html (95%) rename target/{ => site}/apidocs/com/studentgui/apptheming/package-tree.html (96%) rename target/{ => site}/apidocs/com/studentgui/apptheming/package-use.html (96%) rename target/{ => site}/apidocs/com/studentgui/bootstrap/Bootstrap.html (94%) rename target/{ => site}/apidocs/com/studentgui/bootstrap/class-use/Bootstrap.html (96%) rename target/{ => site}/apidocs/com/studentgui/bootstrap/package-summary.html (97%) rename target/{ => site}/apidocs/com/studentgui/bootstrap/package-tree.html (96%) rename target/{ => site}/apidocs/com/studentgui/bootstrap/package-use.html (96%) rename target/{ => site}/apidocs/com/studentgui/test/BrailleSmokeTest.html (96%) rename target/{ => site}/apidocs/com/studentgui/test/class-use/BrailleSmokeTest.html (96%) rename target/{ => site}/apidocs/com/studentgui/test/package-summary.html (97%) rename target/{ => site}/apidocs/com/studentgui/test/package-tree.html (96%) rename target/{ => site}/apidocs/com/studentgui/test/package-use.html (96%) rename target/{ => site}/apidocs/com/studentgui/tools/GroupedSmoke.html (77%) rename target/{ => site}/apidocs/com/studentgui/tools/ProgrammaticPageSaveTest.html (72%) rename target/{ => site}/apidocs/com/studentgui/tools/QueryStudentData.html (80%) rename target/{ => site}/apidocs/com/studentgui/tools/RenderStudentProgress.html (78%) rename target/{ => site}/apidocs/com/studentgui/tools/SmokeTest.html (78%) rename target/{ => site}/apidocs/com/studentgui/tools/class-use/GroupedSmoke.html (96%) rename target/{ => site}/apidocs/com/studentgui/tools/class-use/ProgrammaticPageSaveTest.html (96%) rename target/{ => site}/apidocs/com/studentgui/tools/class-use/QueryStudentData.html (96%) rename target/{ => site}/apidocs/com/studentgui/tools/class-use/RenderStudentProgress.html (96%) rename target/{ => site}/apidocs/com/studentgui/tools/class-use/SmokeTest.html (96%) rename target/{ => site}/apidocs/com/studentgui/tools/package-summary.html (86%) rename target/{ => site}/apidocs/com/studentgui/tools/package-tree.html (97%) rename target/{ => site}/apidocs/com/studentgui/tools/package-use.html (96%) rename target/{ => site}/apidocs/com/studentgui/uicomp/PhaseScoreField.html (97%) rename target/{ => site}/apidocs/com/studentgui/uicomp/class-use/PhaseScoreField.html (96%) rename target/{ => site}/apidocs/com/studentgui/uicomp/package-summary.html (97%) rename target/{ => site}/apidocs/com/studentgui/uicomp/package-tree.html (97%) rename target/{ => site}/apidocs/com/studentgui/uicomp/package-use.html (96%) rename target/{ => site}/apidocs/copy.svg (100%) rename target/{ => site}/apidocs/deprecated-list.html (97%) rename target/{ => site}/apidocs/element-list (100%) rename target/{ => site}/apidocs/help-doc.html (98%) rename target/{ => site}/apidocs/index-all.html (97%) rename target/{ => site}/apidocs/index.html (97%) create mode 100644 target/site/apidocs/legal/ADDITIONAL_LICENSE_INFO create mode 100644 target/site/apidocs/legal/ASSEMBLY_EXCEPTION create mode 100644 target/site/apidocs/legal/LICENSE rename target/{ => site}/apidocs/legal/jquery.md (100%) rename target/{ => site}/apidocs/legal/jqueryUI.md (100%) rename target/{ => site}/apidocs/link.svg (100%) rename target/{ => site}/apidocs/member-search-index.js (100%) rename target/{ => site}/apidocs/module-search-index.js (100%) rename target/{ => site}/apidocs/overview-summary.html (87%) rename target/{ => site}/apidocs/overview-tree.html (99%) rename target/{ => site}/apidocs/package-search-index.js (100%) rename target/{ => site}/apidocs/package-summary.html (96%) rename target/{ => site}/apidocs/package-tree.html (96%) rename target/{ => site}/apidocs/package-use.html (95%) rename target/{ => site}/apidocs/resources/glass.png (100%) rename target/{ => site}/apidocs/resources/x.png (100%) rename target/{ => site}/apidocs/script-dir/jquery-3.7.1.min.js (100%) rename target/{ => site}/apidocs/script-dir/jquery-ui.min.css (100%) rename target/{ => site}/apidocs/script-dir/jquery-ui.min.js (100%) rename target/{ => site}/apidocs/script.js (100%) rename target/{ => site}/apidocs/search-page.js (100%) rename target/{ => site}/apidocs/search.html (96%) rename target/{ => site}/apidocs/search.js (100%) rename target/{ => site}/apidocs/serialized-form.html (99%) create mode 100644 target/site/apidocs/src-html/VersionUtil.html create mode 100644 target/site/apidocs/src-html/com/studentgui/app/DateChangeListener.html create mode 100644 target/site/apidocs/src-html/com/studentgui/app/Main.html create mode 100644 target/site/apidocs/src-html/com/studentgui/app/PreferencesDialog.html create mode 100644 target/site/apidocs/src-html/com/studentgui/app/SettingsChangeListener.html create mode 100644 target/site/apidocs/src-html/com/studentgui/app/StudentChangeListener.html create mode 100644 target/site/apidocs/src-html/com/studentgui/apphelpers/Database.ResultsWithDates.html create mode 100644 target/site/apidocs/src-html/com/studentgui/apphelpers/Database.html create mode 100644 target/site/apidocs/src-html/com/studentgui/apphelpers/Helpers.html create mode 100644 target/site/apidocs/src-html/com/studentgui/apphelpers/PythonPlotter.html create mode 100644 target/site/apidocs/src-html/com/studentgui/apphelpers/SessionJsonWriter.html create mode 100644 target/site/apidocs/src-html/com/studentgui/apphelpers/Settings.html create mode 100644 target/site/apidocs/src-html/com/studentgui/apphelpers/SqlGenerate.html create mode 100644 target/site/apidocs/src-html/com/studentgui/apphelpers/UiNotifier.html create mode 100644 target/site/apidocs/src-html/com/studentgui/apphelpers/dto/AssessmentPayload.html create mode 100644 target/site/apidocs/src-html/com/studentgui/apphelpers/dto/ContactPayload.html create mode 100644 target/site/apidocs/src-html/com/studentgui/apphelpers/dto/KeyboardingPayload.html create mode 100644 target/site/apidocs/src-html/com/studentgui/apphelpers/dto/NotesPayload.html create mode 100644 target/site/apidocs/src-html/com/studentgui/apphelpers/dto/SessionPayload.html create mode 100644 target/site/apidocs/src-html/com/studentgui/apppages/Abacus.html create mode 100644 target/site/apidocs/src-html/com/studentgui/apppages/Braille.html create mode 100644 target/site/apidocs/src-html/com/studentgui/apppages/BrailleNote.html create mode 100644 target/site/apidocs/src-html/com/studentgui/apppages/BrailleSense.html create mode 100644 target/site/apidocs/src-html/com/studentgui/apppages/CVI.html create mode 100644 target/site/apidocs/src-html/com/studentgui/apppages/ContactLog.html create mode 100644 target/site/apidocs/src-html/com/studentgui/apppages/DigitalLiteracy.html create mode 100644 target/site/apidocs/src-html/com/studentgui/apppages/Homepage.html create mode 100644 target/site/apidocs/src-html/com/studentgui/apppages/IOS.html create mode 100644 target/site/apidocs/src-html/com/studentgui/apppages/InstructionalMaterials.html create mode 100644 target/site/apidocs/src-html/com/studentgui/apppages/JLineGraph.html create mode 100644 target/site/apidocs/src-html/com/studentgui/apppages/Keyboarding.html create mode 100644 target/site/apidocs/src-html/com/studentgui/apppages/Observations.html create mode 100644 target/site/apidocs/src-html/com/studentgui/apppages/ScreenReader.html create mode 100644 target/site/apidocs/src-html/com/studentgui/apppages/SessionNotes.html create mode 100644 target/site/apidocs/src-html/com/studentgui/apptheming/Theme.html create mode 100644 target/site/apidocs/src-html/com/studentgui/bootstrap/Bootstrap.html create mode 100644 target/site/apidocs/src-html/com/studentgui/test/BrailleSmokeTest.html create mode 100644 target/site/apidocs/src-html/com/studentgui/tools/GroupedSmoke.html create mode 100644 target/site/apidocs/src-html/com/studentgui/tools/ProgrammaticPageSaveTest.html create mode 100644 target/site/apidocs/src-html/com/studentgui/tools/QueryStudentData.html create mode 100644 target/site/apidocs/src-html/com/studentgui/tools/RenderStudentProgress.html create mode 100644 target/site/apidocs/src-html/com/studentgui/tools/SmokeTest.html create mode 100644 target/site/apidocs/src-html/com/studentgui/uicomp/PhaseScoreField.html rename target/{ => site}/apidocs/stylesheet.css (100%) rename target/{ => site}/apidocs/tag-search-index.js (100%) rename target/{ => site}/apidocs/type-search-index.js (100%) create mode 100644 target/site/checkstyle.html create mode 100644 target/site/css/maven-base.css create mode 100644 target/site/css/maven-theme.css create mode 100644 target/site/css/print.css create mode 100644 target/site/css/site.css create mode 100644 target/site/dependencies.html create mode 100644 target/site/dependency-info.html create mode 100644 target/site/images/close.gif create mode 100644 target/site/images/collapsed.gif create mode 100644 target/site/images/expanded.gif create mode 100644 target/site/images/external.png create mode 100644 target/site/images/icon_error_sml.gif create mode 100644 target/site/images/icon_info_sml.gif create mode 100644 target/site/images/icon_success_sml.gif create mode 100644 target/site/images/icon_warning_sml.gif create mode 100644 target/site/images/logos/build-by-maven-black.png create mode 100644 target/site/images/logos/build-by-maven-white.png create mode 100644 target/site/images/logos/maven-feather.png create mode 100644 target/site/images/newwindow.png create mode 100644 target/site/index.html create mode 100644 target/site/licenses.html create mode 100644 target/site/plugins.html create mode 100644 target/site/project-info.html create mode 100644 target/site/project-reports.html create mode 100644 target/site/summary.html create mode 100644 target/site/surefire.html create mode 100644 target/site/testapidocs/allclasses-index.html create mode 100644 target/site/testapidocs/allpackages-index.html create mode 100644 target/site/testapidocs/com/studentgui/apphelpers/DatabaseContactLogTest.html create mode 100644 target/site/testapidocs/com/studentgui/apphelpers/SessionJsonWriterTest.html create mode 100644 target/site/testapidocs/com/studentgui/apphelpers/SqlGenerateTest.html create mode 100644 target/site/testapidocs/com/studentgui/apphelpers/class-use/DatabaseContactLogTest.html create mode 100644 target/site/testapidocs/com/studentgui/apphelpers/class-use/SessionJsonWriterTest.html create mode 100644 target/site/testapidocs/com/studentgui/apphelpers/class-use/SqlGenerateTest.html create mode 100644 target/site/testapidocs/com/studentgui/apphelpers/package-summary.html create mode 100644 target/site/testapidocs/com/studentgui/apphelpers/package-tree.html create mode 100644 target/site/testapidocs/com/studentgui/apphelpers/package-use.html create mode 100644 target/site/testapidocs/com/studentgui/apppages/JLineGraphDeterministicJitterTest.html create mode 100644 target/site/testapidocs/com/studentgui/apppages/class-use/JLineGraphDeterministicJitterTest.html create mode 100644 target/site/testapidocs/com/studentgui/apppages/package-summary.html create mode 100644 target/site/testapidocs/com/studentgui/apppages/package-tree.html create mode 100644 target/site/testapidocs/com/studentgui/apppages/package-use.html create mode 100644 target/site/testapidocs/com/studentgui/test/BrailleDatabaseTest.html create mode 100644 target/site/testapidocs/com/studentgui/test/BrailleSmokeTest.html create mode 100644 target/site/testapidocs/com/studentgui/test/DatabaseEdgeCasesTest.html create mode 100644 target/site/testapidocs/com/studentgui/test/DatabaseTest.html create mode 100644 target/site/testapidocs/com/studentgui/test/ExportBrailleReportsTest.html create mode 100644 target/site/testapidocs/com/studentgui/test/class-use/BrailleDatabaseTest.html create mode 100644 target/site/testapidocs/com/studentgui/test/class-use/BrailleSmokeTest.html create mode 100644 target/site/testapidocs/com/studentgui/test/class-use/DatabaseEdgeCasesTest.html create mode 100644 target/site/testapidocs/com/studentgui/test/class-use/DatabaseTest.html create mode 100644 target/site/testapidocs/com/studentgui/test/class-use/ExportBrailleReportsTest.html create mode 100644 target/site/testapidocs/com/studentgui/test/package-summary.html create mode 100644 target/site/testapidocs/com/studentgui/test/package-tree.html create mode 100644 target/site/testapidocs/com/studentgui/test/package-use.html create mode 100644 target/site/testapidocs/copy.svg create mode 100644 target/site/testapidocs/element-list create mode 100644 target/site/testapidocs/help-doc.html create mode 100644 target/site/testapidocs/index-all.html create mode 100644 target/site/testapidocs/index.html create mode 100644 target/site/testapidocs/legal/ADDITIONAL_LICENSE_INFO create mode 100644 target/site/testapidocs/legal/ASSEMBLY_EXCEPTION create mode 100644 target/site/testapidocs/legal/LICENSE create mode 100644 target/site/testapidocs/legal/jquery.md create mode 100644 target/site/testapidocs/legal/jqueryUI.md create mode 100644 target/site/testapidocs/link.svg create mode 100644 target/site/testapidocs/member-search-index.js create mode 100644 target/site/testapidocs/module-search-index.js create mode 100644 target/site/testapidocs/overview-summary.html create mode 100644 target/site/testapidocs/overview-tree.html create mode 100644 target/site/testapidocs/package-search-index.js create mode 100644 target/site/testapidocs/resources/glass.png create mode 100644 target/site/testapidocs/resources/x.png create mode 100644 target/site/testapidocs/script-dir/jquery-3.7.1.min.js create mode 100644 target/site/testapidocs/script-dir/jquery-ui.min.css create mode 100644 target/site/testapidocs/script-dir/jquery-ui.min.js create mode 100644 target/site/testapidocs/script.js create mode 100644 target/site/testapidocs/search-page.js create mode 100644 target/site/testapidocs/search.html create mode 100644 target/site/testapidocs/search.js create mode 100644 target/site/testapidocs/src-html/com/studentgui/apphelpers/DatabaseContactLogTest.html create mode 100644 target/site/testapidocs/src-html/com/studentgui/apphelpers/SessionJsonWriterTest.html create mode 100644 target/site/testapidocs/src-html/com/studentgui/apphelpers/SqlGenerateTest.html create mode 100644 target/site/testapidocs/src-html/com/studentgui/apppages/JLineGraphDeterministicJitterTest.html create mode 100644 target/site/testapidocs/src-html/com/studentgui/test/BrailleDatabaseTest.html create mode 100644 target/site/testapidocs/src-html/com/studentgui/test/BrailleSmokeTest.html create mode 100644 target/site/testapidocs/src-html/com/studentgui/test/DatabaseEdgeCasesTest.html create mode 100644 target/site/testapidocs/src-html/com/studentgui/test/DatabaseTest.html create mode 100644 target/site/testapidocs/src-html/com/studentgui/test/ExportBrailleReportsTest.html create mode 100644 target/site/testapidocs/stylesheet.css create mode 100644 target/site/testapidocs/tag-search-index.js create mode 100644 target/site/testapidocs/type-search-index.js delete mode 100644 target/surefire-reports/2025-10-29T13-28-26_658.dumpstream diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml new file mode 100644 index 0000000..c297cec --- /dev/null +++ b/.github/workflows/ci.yml @@ -0,0 +1,24 @@ +name: CI + +on: + push: + branches: [ "**" ] + pull_request: + branches: [ "**" ] + +jobs: + build: + runs-on: ubuntu-latest + steps: + - name: Checkout + uses: actions/checkout@v4 + + - name: Set up JDK 21 + uses: actions/setup-java@v4 + with: + distribution: temurin + java-version: '21' + cache: maven + + - name: Build and test + run: mvn -B -U -e -V clean verify diff --git a/.github/workflows/package-linux.yml b/.github/workflows/package-linux.yml new file mode 100644 index 0000000..520910e --- /dev/null +++ b/.github/workflows/package-linux.yml @@ -0,0 +1,130 @@ +name: Package Linux (deb, rpm, AppImage) + +on: + workflow_dispatch: {} + +env: + APP_NAME: StudentDataGUI + +jobs: + linux-packages: + runs-on: ubuntu-latest + steps: + - name: Checkout + uses: actions/checkout@v4 + + - name: Set up JDK 21 (with jpackage) + uses: actions/setup-java@v4 + with: + distribution: temurin + java-version: '21' + cache: maven + + - name: Prepare tooling + run: | + sudo apt-get update + # For building RPMs via jpackage + sudo apt-get install -y rpm + # Optional: tools often needed by AppImage flows + sudo apt-get install -y libfuse2 zsync + + - name: Resolve project version + id: meta + shell: bash + run: | + set -euo pipefail + VERSION=$(mvn -q -Dexec.cleanupDaemonThreads=false -DforceStdout help:evaluate -Dexpression=project.version) + echo "version=$VERSION" >> "$GITHUB_OUTPUT" + echo "vversion=v$VERSION" >> "$GITHUB_OUTPUT" + + - name: Build JAR + run: mvn -B -U -e -DskipTests package + + - name: Locate built JAR + id: jar + run: | + set -euo pipefail + JAR=$(ls -t target/*-shaded.jar 2>/dev/null | head -n1) + if [ -z "$JAR" ]; then JAR=$(ls -t target/*.jar | grep -vE 'sources|original|tests' | head -n1); fi + if [ -z "$JAR" ]; then echo "No JAR found in target" >&2; exit 1; fi + echo "path=$JAR" >> "$GITHUB_OUTPUT" + echo "file=$(basename "$JAR")" >> "$GITHUB_OUTPUT" + + - name: Create output directories + run: | + mkdir -p dist/linux/appimage dist/linux/deb dist/linux/rpm + + - name: Build DEB (jpackage) + run: | + "$JAVA_HOME/bin/jpackage" \ + --type deb \ + --input target \ + --main-jar "${{ steps.jar.outputs.file }}" \ + --name "$APP_NAME" \ + --app-version "${{ steps.meta.outputs.version }}" \ + --icon examples/icons/scatter-plot-256.png \ + --dest dist/linux/deb + f=$(ls -t dist/linux/deb/*.deb | head -n1); if [ -n "$f" ]; then mv "$f" "dist/linux/deb/${APP_NAME}-${{ steps.meta.outputs.vversion }}.deb"; fi + + - name: Build RPM (jpackage) + run: | + # jpackage requires rpm-build; installed earlier + "$JAVA_HOME/bin/jpackage" \ + --type rpm \ + --input target \ + --main-jar "${{ steps.jar.outputs.file }}" \ + --name "$APP_NAME" \ + --app-version "${{ steps.meta.outputs.version }}" \ + --icon examples/icons/scatter-plot-256.png \ + --dest dist/linux/rpm + f=$(ls -t dist/linux/rpm/*.rpm | head -n1); if [ -n "$f" ]; then mv "$f" "dist/linux/rpm/${APP_NAME}-${{ steps.meta.outputs.vversion }}.rpm"; fi + + - name: Build AppImage (via appimagetool if available) + continue-on-error: true + shell: bash + run: | + set -euxo pipefail + APPDIR_ROOT="dist/linux/appimage" + "$JAVA_HOME/bin/jpackage" \ + --type app-image \ + --input target \ + --main-jar "${{ steps.jar.outputs.file }}" \ + --name "$APP_NAME" \ + --app-version "${{ steps.meta.outputs.version }}" \ + --icon examples/icons/scatter-plot-256.png \ + --dest "$APPDIR_ROOT" + + APPDIR=$(find "$APPDIR_ROOT" -maxdepth 1 -type d -name "*${APP_NAME}*" | head -n1) + if [ -z "$APPDIR" ]; then echo "Could not find jpackage app-image directory" >&2; exit 1; fi + + # Try to fetch appimagetool and build .AppImage; fall back to zipping the app-image directory + mkdir -p "$APPDIR_ROOT/out" + pushd "$APPDIR_ROOT" >/dev/null + APPIMAGE_TOOL_URL="https://github.com/AppImage/AppImageKit/releases/download/13/appimagetool-x86_64.AppImage" + curl -fsSL "$APPIMAGE_TOOL_URL" -o appimagetool + chmod +x appimagetool || true + if ./appimagetool --appimage-extract-and-run "$(basename "$APPDIR")"; then + AI=$(ls -t "$APPDIR_ROOT"/*.AppImage 2>/dev/null | head -n1 || true) + if [ -n "$AI" ]; then mv "$AI" "$APPDIR_ROOT/${APP_NAME}-${{ steps.meta.outputs.vversion }}.AppImage"; fi + echo "AppImage built successfully" + else + echo "appimagetool failed; creating zip of app-image directory instead" >&2 + zip -r "${APP_NAME}-${{ steps.meta.outputs.vversion }}-appimage.zip" "$(basename "$APPDIR")" || echo "Failed to create zip archive" + fi + popd >/dev/null + + - name: Upload DEB and RPM artifacts (core Linux packages) + uses: actions/upload-artifact@v4 + with: + name: linux-packages + path: | + dist/linux/deb/** + dist/linux/rpm/** + + - name: Upload AppImage artifacts (optional - may fail) + continue-on-error: true + uses: actions/upload-artifact@v4 + with: + name: linux-packages + path: | + dist/linux/appimage/** diff --git a/.github/workflows/package-macos.yml b/.github/workflows/package-macos.yml new file mode 100644 index 0000000..b086227 --- /dev/null +++ b/.github/workflows/package-macos.yml @@ -0,0 +1,70 @@ +name: Package macOS (DMG) + +on: + workflow_dispatch: {} + +env: + APP_NAME: StudentDataGUI + +jobs: + macos-dmg: + runs-on: macos-latest + steps: + - name: Checkout + uses: actions/checkout@v4 + + - name: Set up JDK 21 (with jpackage) + uses: actions/setup-java@v4 + with: + distribution: temurin + java-version: '21' + cache: maven + + - name: Resolve project meta + id: meta + shell: bash + run: | + set -euo pipefail + VERSION=$(mvn -q -Dexec.cleanupDaemonThreads=false -DforceStdout help:evaluate -Dexpression=project.version) + echo "version=$VERSION" >> "$GITHUB_OUTPUT" + echo "vversion=v$VERSION" >> "$GITHUB_OUTPUT" + + - name: Build JAR + run: mvn -B -U -e -DskipTests package + + - name: Generate .icns from PNG set + run: | + chmod +x examples/scripts/create-mac-iconset.sh + # Use the provided PNGs as source; output an icns in the workspace root + examples/scripts/create-mac-iconset.sh examples/icons scatter-plot.icns + + - name: Locate built JAR + id: jar + run: | + set -euo pipefail + JAR=$(ls -t target/*-shaded.jar 2>/dev/null | head -n1) + if [ -z "$JAR" ]; then JAR=$(ls -t target/*.jar | grep -vE 'sources|original|tests' | head -n1); fi + if [ -z "$JAR" ]; then echo "No JAR found in target" >&2; exit 1; fi + echo "path=$JAR" >> "$GITHUB_OUTPUT" + echo "file=$(basename "$JAR")" >> "$GITHUB_OUTPUT" + + - name: Create output directory + run: mkdir -p dist/macos + + - name: Build DMG (jpackage) + run: | + "$JAVA_HOME/bin/jpackage" \ + --type dmg \ + --input target \ + --main-jar "${{ steps.jar.outputs.file }}" \ + --name "$APP_NAME" \ + --app-version "${{ steps.meta.outputs.version }}" \ + --icon scatter-plot.icns \ + --dest dist/macos + f=$(ls -t dist/macos/*.dmg | head -n1); if [ -n "$f" ]; then mv "$f" "dist/macos/${APP_NAME}-${{ steps.meta.outputs.vversion }}.dmg"; fi + + - name: Upload DMG artifact + uses: actions/upload-artifact@v4 + with: + name: macos-dmg + path: dist/macos/*.dmg diff --git a/.github/workflows/package-windows.yml b/.github/workflows/package-windows.yml new file mode 100644 index 0000000..fa18783 --- /dev/null +++ b/.github/workflows/package-windows.yml @@ -0,0 +1,68 @@ +name: Package Windows (MSI) + +on: + workflow_dispatch: {} + +env: + APP_NAME: StudentDataGUI + +jobs: + windows-msi: + runs-on: windows-latest + steps: + - name: Checkout + uses: actions/checkout@v4 + + - name: Set up JDK 21 (with jpackage) + uses: actions/setup-java@v4 + with: + distribution: temurin + java-version: '21' + cache: maven + + - name: Resolve project meta + id: meta + shell: bash + run: | + set -euo pipefail + VERSION=$(mvn -q -Dexec.cleanupDaemonThreads=false -DforceStdout help:evaluate -Dexpression=project.version) + echo "version=$VERSION" >> "$GITHUB_OUTPUT" + echo "vversion=v$VERSION" >> "$GITHUB_OUTPUT" + + - name: Build JAR + run: mvn -B -U -e -DskipTests package + + - name: Locate built JAR + id: jar + shell: pwsh + run: | + $jar = Get-ChildItem -Path target -Filter "*-shaded.jar" -File | Sort-Object LastWriteTime -Descending | Select-Object -First 1 + if (-not $jar) { $jar = Get-ChildItem -Path target -Filter "*.jar" -File | Where-Object { $_.Name -notmatch 'sources|original|tests' } | Sort-Object LastWriteTime -Descending | Select-Object -First 1 } + if (-not $jar) { throw "No JAR found in target" } + "path=$($jar.FullName)" | Out-File -FilePath $env:GITHUB_OUTPUT -Encoding utf8 -Append + "file=$($jar.Name)" | Out-File -FilePath $env:GITHUB_OUTPUT -Encoding utf8 -Append + + - name: Create output directory + shell: pwsh + run: New-Item -ItemType Directory -Path dist\windows -Force | Out-Null + + - name: Build MSI (jpackage) + shell: pwsh + run: | + & "$env:JAVA_HOME\bin\jpackage.exe" ` + --type msi ` + --input target ` + --main-jar "${{ steps.jar.outputs.file }}" ` + --name "$env:APP_NAME" ` + --app-version "${{ steps.meta.outputs.version }}" ` + --icon examples\icons\scatter-plot-256.ico ` + --dest dist\windows ` + --win-menu --win-shortcut --win-dir-chooser + $msi = Get-ChildItem -Path dist\windows -Filter "*.msi" -File | Sort-Object LastWriteTime -Descending | Select-Object -First 1 + if ($msi) { Move-Item -Path $msi.FullName -Destination ("dist\windows\{0}-{1}.msi" -f $env:APP_NAME, "${{ steps.meta.outputs.vversion }}") -Force } + + - name: Upload MSI artifact + uses: actions/upload-artifact@v4 + with: + name: windows-msi + path: dist/windows/*.msi diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml new file mode 100644 index 0000000..13594c0 --- /dev/null +++ b/.github/workflows/release.yml @@ -0,0 +1,177 @@ +name: Build and Attach Installers on Release + +on: + release: + types: [published] + workflow_dispatch: {} + +permissions: + contents: write + +env: + JAVA_VERSION: '21' + APP_NAME: StudentDataGUI + +jobs: + linux: + name: Linux packages (deb, rpm, AppImage) + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + - uses: actions/setup-java@v4 + with: + distribution: temurin + java-version: ${{ env.JAVA_VERSION }} + cache: maven + - name: Install packaging tools + run: | + sudo apt-get update + sudo apt-get install -y rpm libfuse2 zsync + - name: Build JAR + run: mvn -B -U -e -DskipTests package + - name: Resolve meta + id: meta + run: | + set -euo pipefail + VERSION=$(mvn -q -Dexec.cleanupDaemonThreads=false -DforceStdout help:evaluate -Dexpression=project.version) + echo "version=$VERSION" >> "$GITHUB_OUTPUT" + echo "vversion=v$VERSION" >> "$GITHUB_OUTPUT" + - name: Find jar + id: jar + run: | + JAR=$(ls -t target/*-shaded.jar 2>/dev/null | head -n1) + if [ -z "$JAR" ]; then JAR=$(ls -t target/*.jar | grep -vE 'sources|original|tests' | head -n1); fi + echo "path=$JAR" >> "$GITHUB_OUTPUT" + echo "file=$(basename "$JAR")" >> "$GITHUB_OUTPUT" + - name: Build deb + run: | + mkdir -p dist/linux/deb + "$JAVA_HOME/bin/jpackage" --type deb --input target --main-jar "${{ steps.jar.outputs.file }}" --name "$APP_NAME" --app-version "${{ steps.meta.outputs.version }}" --icon examples/icons/scatter-plot-256.png --dest dist/linux/deb + f=$(ls -t dist/linux/deb/*.deb | head -n1); if [ -n "$f" ]; then mv "$f" "dist/linux/deb/${APP_NAME}-${{ steps.meta.outputs.vversion }}.deb"; fi + - name: Build rpm + run: | + mkdir -p dist/linux/rpm + "$JAVA_HOME/bin/jpackage" --type rpm --input target --main-jar "${{ steps.jar.outputs.file }}" --name "$APP_NAME" --app-version "${{ steps.meta.outputs.version }}" --icon examples/icons/scatter-plot-256.png --dest dist/linux/rpm + f=$(ls -t dist/linux/rpm/*.rpm | head -n1); if [ -n "$f" ]; then mv "$f" "dist/linux/rpm/${APP_NAME}-${{ steps.meta.outputs.vversion }}.rpm"; fi + - name: Build AppImage + run: | + set -euxo pipefail + APPDIR_ROOT="dist/linux/appimage" + "$JAVA_HOME/bin/jpackage" --type app-image --input target --main-jar "${{ steps.jar.outputs.file }}" --name "$APP_NAME" --app-version "${{ steps.meta.outputs.version }}" --icon examples/icons/scatter-plot-256.png --dest "$APPDIR_ROOT" + pushd "$APPDIR_ROOT" + APPDIR=$(find . -maxdepth 1 -type d -name "*${APP_NAME}*" | head -n1 | sed 's#^./##') + curl -fsSL https://github.com/AppImage/AppImageKit/releases/download/13/appimagetool-x86_64.AppImage -o appimagetool + chmod +x appimagetool || true + if ./appimagetool --appimage-extract-and-run "$APPDIR"; then + AI=$(ls -t *.AppImage 2>/dev/null | head -n1 || true) + if [ -n "$AI" ]; then mv "$AI" "${APP_NAME}-${{ steps.meta.outputs.vversion }}.AppImage"; fi + echo "Built AppImage" + else + zip -r "${APP_NAME}-${{ steps.meta.outputs.vversion }}-appimage.zip" "$APPDIR" + fi + popd + - name: Upload assets to release + uses: softprops/action-gh-release@v2 + with: + files: | + dist/linux/**/*.deb + dist/linux/**/*.rpm + dist/linux/**/*.AppImage + dist/linux/**/*.zip + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + + windows: + name: Windows MSI + runs-on: windows-latest + steps: + - uses: actions/checkout@v4 + - uses: actions/setup-java@v4 + with: + distribution: temurin + java-version: ${{ env.JAVA_VERSION }} + cache: maven + - name: Build JAR + run: mvn -B -U -e -DskipTests package + - name: Resolve meta + id: meta + shell: bash + run: | + set -euo pipefail + VERSION=$(mvn -q -Dexec.cleanupDaemonThreads=false -DforceStdout help:evaluate -Dexpression=project.version) + echo "version=$VERSION" >> "$GITHUB_OUTPUT" + echo "vversion=v$VERSION" >> "$GITHUB_OUTPUT" + - name: Find jar + id: jar + shell: pwsh + run: | + $jar = Get-ChildItem -Path target -Filter "*-shaded.jar" -File | Sort-Object LastWriteTime -Descending | Select-Object -First 1 + if (-not $jar) { $jar = Get-ChildItem -Path target -Filter "*.jar" -File | Where-Object { $_.Name -notmatch 'sources|original|tests' } | Sort-Object LastWriteTime -Descending | Select-Object -First 1 } + if (-not $jar) { throw "No JAR found in target" } + "path=$($jar.FullName)" | Out-File -FilePath $env:GITHUB_OUTPUT -Encoding utf8 -Append + "file=$($jar.Name)" | Out-File -FilePath $env:GITHUB_OUTPUT -Encoding utf8 -Append + - name: Build MSI + shell: pwsh + run: | + New-Item -ItemType Directory -Path dist\windows -Force | Out-Null + & "$env:JAVA_HOME\bin\jpackage.exe" ` + --type msi ` + --input target ` + --main-jar "${{ steps.jar.outputs.file }}" ` + --name "$env:APP_NAME" ` + --app-version "${{ steps.meta.outputs.version }}" ` + --icon examples\icons\scatter-plot-256.ico ` + --dest dist\windows ` + --win-menu --win-shortcut --win-dir-chooser + $msi = Get-ChildItem -Path dist\windows -Filter "*.msi" -File | Sort-Object LastWriteTime -Descending | Select-Object -First 1 + if ($msi) { Move-Item -Path $msi.FullName -Destination ("dist\windows\{0}-{1}.msi" -f $env:APP_NAME, "${{ steps.meta.outputs.vversion }}") -Force } + - name: Upload assets to release + uses: softprops/action-gh-release@v2 + with: + files: | + dist/windows/*.msi + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + + macos: + name: macOS DMG + runs-on: macos-latest + steps: + - uses: actions/checkout@v4 + - uses: actions/setup-java@v4 + with: + distribution: temurin + java-version: ${{ env.JAVA_VERSION }} + cache: maven + - name: Build JAR + run: mvn -B -U -e -DskipTests package + - name: Resolve meta + id: meta + run: | + set -euo pipefail + VERSION=$(mvn -q -Dexec.cleanupDaemonThreads=false -DforceStdout help:evaluate -Dexpression=project.version) + echo "version=$VERSION" >> "$GITHUB_OUTPUT" + echo "vversion=v$VERSION" >> "$GITHUB_OUTPUT" + - name: Generate .icns + run: | + chmod +x examples/scripts/create-mac-iconset.sh + examples/scripts/create-mac-iconset.sh examples/icons scatter-plot.icns + - name: Find jar + id: jar + run: | + JAR=$(ls -t target/*-shaded.jar 2>/dev/null | head -n1) + if [ -z "$JAR" ]; then JAR=$(ls -t target/*.jar | grep -vE 'sources|original|tests' | head -n1); fi + echo "path=$JAR" >> "$GITHUB_OUTPUT" + echo "file=$(basename "$JAR")" >> "$GITHUB_OUTPUT" + - name: Build DMG + run: | + mkdir -p dist/macos + "$JAVA_HOME/bin/jpackage" --type dmg --input target --main-jar "${{ steps.jar.outputs.file }}" --name "$APP_NAME" --app-version "${{ steps.meta.outputs.version }}" --icon scatter-plot.icns --dest dist/macos + f=$(ls -t dist/macos/*.dmg | head -n1); if [ -n "$f" ]; then mv "$f" "dist/macos/${APP_NAME}-${{ steps.meta.outputs.vversion }}.dmg"; fi + - name: Upload assets to release + uses: softprops/action-gh-release@v2 + with: + files: | + dist/macos/*.dmg + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} diff --git a/.github/workflows/site-deploy.yml b/.github/workflows/site-deploy.yml new file mode 100644 index 0000000..2df20cc --- /dev/null +++ b/.github/workflows/site-deploy.yml @@ -0,0 +1,59 @@ +name: Deploy Maven Site to GitHub Pages + +on: + workflow_dispatch: {} + push: + branches: + - main + - master + tags: + - 'v*.*' # match minor tags like v1.2 + - '*.*' # also match non-v minor tags like 1.2 + tags-ignore: + - 'v*.*.*' # ignore patch tags like v1.2.3 + - '*.*.*' # ignore non-v patch tags like 1.2.3 + +permissions: + contents: read + pages: write + id-token: write + +concurrency: + group: "pages" + cancel-in-progress: true + +env: + JAVA_VERSION: '21' + +jobs: + build: + runs-on: ubuntu-latest + steps: + - name: Checkout + uses: actions/checkout@v4 + + - name: Set up JDK + uses: actions/setup-java@v4 + with: + distribution: temurin + java-version: ${{ env.JAVA_VERSION }} + cache: maven + + - name: Build Maven Site + run: mvn -B -U -e -V clean site + + - name: Upload Pages artifact + uses: actions/upload-pages-artifact@v3 + with: + path: target/site + + deploy: + environment: + name: github-pages + url: ${{ steps.deployment.outputs.page_url }} + runs-on: ubuntu-latest + needs: build + steps: + - name: Deploy to GitHub Pages + id: deployment + uses: actions/deploy-pages@v4 diff --git a/.github/workflows/site.yml b/.github/workflows/site.yml new file mode 100644 index 0000000..5d87657 --- /dev/null +++ b/.github/workflows/site.yml @@ -0,0 +1,27 @@ +name: Maven Site + +on: + workflow_dispatch: {} + +jobs: + site: + runs-on: ubuntu-latest + steps: + - name: Checkout + uses: actions/checkout@v4 + + - name: Set up JDK 21 + uses: actions/setup-java@v4 + with: + distribution: temurin + java-version: '21' + cache: maven + + - name: Generate site + run: mvn -B -U -e -V clean site + + - name: Upload site artifact + uses: actions/upload-artifact@v4 + with: + name: maven-site + path: target/site diff --git a/.gitignore b/.gitignore index 61df171..063f33f 100644 --- a/.gitignore +++ b/.gitignore @@ -1,26 +1,26 @@ -# Compiled class file -*.class - -# Log file -*.log - -# BlueJ files -*.ctxt - -# Mobile Tools for Java (J2ME) -.mtj.tmp/ - -# Package Files # -*.jar -*.war -*.nar -*.ear -*.zip -*.tar.gz -*.rar - -# virtual machine crash logs, see http://www.java.com/en/download/help/error_hotspot.xml -hs_err_pid* -replay_pid* -students.json +# Compiled class file +*.class + +# Log file +*.log + +# BlueJ files +*.ctxt + +# Mobile Tools for Java (J2ME) +.mtj.tmp/ + +# Package Files # +*.jar +*.war +*.nar +*.ear +*.zip +*.tar.gz +*.rar + +# virtual machine crash logs, see http://www.java.com/en/download/help/error_hotspot.xml +hs_err_pid* +replay_pid* +students.json app_home/ \ No newline at end of file diff --git a/.vscode/settings.json b/.vscode/settings.json index c5f3f6b..0d73912 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -1,3 +1,3 @@ -{ - "java.configuration.updateBuildConfiguration": "interactive" +{ + "java.configuration.updateBuildConfiguration": "interactive" } \ No newline at end of file diff --git a/CHANGELOG.md b/CHANGELOG.md index 638bdbe..69cfb79 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,66 +1,66 @@ -# Changelog - -All notable changes to this project should be documented in this file. -This changelog uses a simple, developer-friendly format and is intended -for maintainers reviewing branches and releases. - -## [Unreleased] - refactor/exception-cleanup - -### Added - -- PreferencesDialog: a centralized runtime preferences UI that persists - settings to the app properties and notifies running components when - preferences change. The dialog exposes jitter controls (enable/disable, - deterministic/seeded) and persists the choices under the `jitter.*` - keys. -- SettingsChangeListener: a small listener interface used to notify - components about runtime preference changes. `JLineGraph` implements - this interface and applies persisted settings when notified. -- Deterministic jitter unit test: `JLineGraphDeterministicJitterTest` - verifies that two `JLineGraph` instances seeded identically produce the - same jitter sequence (render-only behavior is validated via reflection). - -### Changed - -- JLineGraph: jitter rendering (±0.10) added as a visual-only effect; new - public APIs to toggle jitter and set the seed (`setJitterEnabled`, - `setJitterDeterministic`, `setJitterSeed`). Horizontal background bands - were added and Y-axis limits are now set to `-0.25 .. 4.25` to match the - visual band ranges. -- Preferences moved to a single canonical UI: the per-chart control bar was - removed from `JLineGraph` in favor of the centralized - `PreferencesDialog`, which persists settings and triggers - `Main.notifySettingsChanged()` so live components update immediately. -- Theme menu: added a guarded "Material Theme UI Lite" submenu exposing a - curated list of Material theme class names when the corresponding - classes are present on the classpath. -- README.md: updated build/run instructions and documented the new - runtime preferences and plotting behavior. - -### Fixed - -- Braille: fixed submitData sizing bug — arrays for part codes and scores - are now allocated dynamically using the actual parts length so persisted - assessment rows align with plotted series. - -### Quality / Docs - -- Javadoc cleanup: multiple small JavaDoc additions and cleanups were - applied across utility classes and app pages to remove doclint warnings. - -### Tests - -- Unit tests: local test run shows all unit tests passing (11 tests, - 0 failures). A smoke utility was added/used to validate chart PNG - export under `app_home/StudentDataFiles/Smoke Test/plots`. - -### Notes - -- Jitter is strictly a rendering effect — stored session data is never - mutated. The runtime preferences are stored under `jitter.enabled`, - `jitter.deterministic`, and `jitter.seed` in the app properties file. -- `Main` exposes `addSettingsChangeListener(...)` / - `removeSettingsChangeListener(...)` and registers the shared - `JLineGraph` instance at startup; if pages later create page-local - `JLineGraph` instances they should register/unregister them with - `Main` to receive runtime preference updates. +# Changelog + +All notable changes to this project should be documented in this file. +This changelog uses a simple, developer-friendly format and is intended +for maintainers reviewing branches and releases. + +## [Unreleased] - refactor/exception-cleanup + +### Added + +- PreferencesDialog: a centralized runtime preferences UI that persists + settings to the app properties and notifies running components when + preferences change. The dialog exposes jitter controls (enable/disable, + deterministic/seeded) and persists the choices under the `jitter.*` + keys. +- SettingsChangeListener: a small listener interface used to notify + components about runtime preference changes. `JLineGraph` implements + this interface and applies persisted settings when notified. +- Deterministic jitter unit test: `JLineGraphDeterministicJitterTest` + verifies that two `JLineGraph` instances seeded identically produce the + same jitter sequence (render-only behavior is validated via reflection). + +### Changed + +- JLineGraph: jitter rendering (±0.10) added as a visual-only effect; new + public APIs to toggle jitter and set the seed (`setJitterEnabled`, + `setJitterDeterministic`, `setJitterSeed`). Horizontal background bands + were added and Y-axis limits are now set to `-0.25 .. 4.25` to match the + visual band ranges. +- Preferences moved to a single canonical UI: the per-chart control bar was + removed from `JLineGraph` in favor of the centralized + `PreferencesDialog`, which persists settings and triggers + `Main.notifySettingsChanged()` so live components update immediately. +- Theme menu: added a guarded "Material Theme UI Lite" submenu exposing a + curated list of Material theme class names when the corresponding + classes are present on the classpath. +- README.md: updated build/run instructions and documented the new + runtime preferences and plotting behavior. + +### Fixed + +- Braille: fixed submitData sizing bug — arrays for part codes and scores + are now allocated dynamically using the actual parts length so persisted + assessment rows align with plotted series. + +### Quality / Docs + +- Javadoc cleanup: multiple small JavaDoc additions and cleanups were + applied across utility classes and app pages to remove doclint warnings. + +### Tests + +- Unit tests: local test run shows all unit tests passing (11 tests, + 0 failures). A smoke utility was added/used to validate chart PNG + export under `app_home/StudentDataFiles/Smoke Test/plots`. + +### Notes + +- Jitter is strictly a rendering effect — stored session data is never + mutated. The runtime preferences are stored under `jitter.enabled`, + `jitter.deterministic`, and `jitter.seed` in the app properties file. +- `Main` exposes `addSettingsChangeListener(...)` / + `removeSettingsChangeListener(...)` and registers the shared + `JLineGraph` instance at startup; if pages later create page-local + `JLineGraph` instances they should register/unregister them with + `Main` to receive runtime preference updates. diff --git a/LICENSE b/LICENSE index 261eeb9..29f81d8 100644 --- a/LICENSE +++ b/LICENSE @@ -1,201 +1,201 @@ - Apache License - Version 2.0, January 2004 - http://www.apache.org/licenses/ - - TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION - - 1. Definitions. - - "License" shall mean the terms and conditions for use, reproduction, - and distribution as defined by Sections 1 through 9 of this document. - - "Licensor" shall mean the copyright owner or entity authorized by - the copyright owner that is granting the License. - - "Legal Entity" shall mean the union of the acting entity and all - other entities that control, are controlled by, or are under common - control with that entity. For the purposes of this definition, - "control" means (i) the power, direct or indirect, to cause the - direction or management of such entity, whether by contract or - otherwise, or (ii) ownership of fifty percent (50%) or more of the - outstanding shares, or (iii) beneficial ownership of such entity. - - "You" (or "Your") shall mean an individual or Legal Entity - exercising permissions granted by this License. - - "Source" form shall mean the preferred form for making modifications, - including but not limited to software source code, documentation - source, and configuration files. - - "Object" form shall mean any form resulting from mechanical - transformation or translation of a Source form, including but - not limited to compiled object code, generated documentation, - and conversions to other media types. - - "Work" shall mean the work of authorship, whether in Source or - Object form, made available under the License, as indicated by a - copyright notice that is included in or attached to the work - (an example is provided in the Appendix below). - - "Derivative Works" shall mean any work, whether in Source or Object - form, that is based on (or derived from) the Work and for which the - editorial revisions, annotations, elaborations, or other modifications - represent, as a whole, an original work of authorship. For the purposes - of this License, Derivative Works shall not include works that remain - separable from, or merely link (or bind by name) to the interfaces of, - the Work and Derivative Works thereof. - - "Contribution" shall mean any work of authorship, including - the original version of the Work and any modifications or additions - to that Work or Derivative Works thereof, that is intentionally - submitted to Licensor for inclusion in the Work by the copyright owner - or by an individual or Legal Entity authorized to submit on behalf of - the copyright owner. For the purposes of this definition, "submitted" - means any form of electronic, verbal, or written communication sent - to the Licensor or its representatives, including but not limited to - communication on electronic mailing lists, source code control systems, - and issue tracking systems that are managed by, or on behalf of, the - Licensor for the purpose of discussing and improving the Work, but - excluding communication that is conspicuously marked or otherwise - designated in writing by the copyright owner as "Not a Contribution." - - "Contributor" shall mean Licensor and any individual or Legal Entity - on behalf of whom a Contribution has been received by Licensor and - subsequently incorporated within the Work. - - 2. Grant of Copyright License. Subject to the terms and conditions of - this License, each Contributor hereby grants to You a perpetual, - worldwide, non-exclusive, no-charge, royalty-free, irrevocable - copyright license to reproduce, prepare Derivative Works of, - publicly display, publicly perform, sublicense, and distribute the - Work and such Derivative Works in Source or Object form. - - 3. Grant of Patent License. Subject to the terms and conditions of - this License, each Contributor hereby grants to You a perpetual, - worldwide, non-exclusive, no-charge, royalty-free, irrevocable - (except as stated in this section) patent license to make, have made, - use, offer to sell, sell, import, and otherwise transfer the Work, - where such license applies only to those patent claims licensable - by such Contributor that are necessarily infringed by their - Contribution(s) alone or by combination of their Contribution(s) - with the Work to which such Contribution(s) was submitted. If You - institute patent litigation against any entity (including a - cross-claim or counterclaim in a lawsuit) alleging that the Work - or a Contribution incorporated within the Work constitutes direct - or contributory patent infringement, then any patent licenses - granted to You under this License for that Work shall terminate - as of the date such litigation is filed. - - 4. Redistribution. You may reproduce and distribute copies of the - Work or Derivative Works thereof in any medium, with or without - modifications, and in Source or Object form, provided that You - meet the following conditions: - - (a) You must give any other recipients of the Work or - Derivative Works a copy of this License; and - - (b) You must cause any modified files to carry prominent notices - stating that You changed the files; and - - (c) You must retain, in the Source form of any Derivative Works - that You distribute, all copyright, patent, trademark, and - attribution notices from the Source form of the Work, - excluding those notices that do not pertain to any part of - the Derivative Works; and - - (d) If the Work includes a "NOTICE" text file as part of its - distribution, then any Derivative Works that You distribute must - include a readable copy of the attribution notices contained - within such NOTICE file, excluding those notices that do not - pertain to any part of the Derivative Works, in at least one - of the following places: within a NOTICE text file distributed - as part of the Derivative Works; within the Source form or - documentation, if provided along with the Derivative Works; or, - within a display generated by the Derivative Works, if and - wherever such third-party notices normally appear. The contents - of the NOTICE file are for informational purposes only and - do not modify the License. You may add Your own attribution - notices within Derivative Works that You distribute, alongside - or as an addendum to the NOTICE text from the Work, provided - that such additional attribution notices cannot be construed - as modifying the License. - - You may add Your own copyright statement to Your modifications and - may provide additional or different license terms and conditions - for use, reproduction, or distribution of Your modifications, or - for any such Derivative Works as a whole, provided Your use, - reproduction, and distribution of the Work otherwise complies with - the conditions stated in this License. - - 5. Submission of Contributions. Unless You explicitly state otherwise, - any Contribution intentionally submitted for inclusion in the Work - by You to the Licensor shall be under the terms and conditions of - this License, without any additional terms or conditions. - Notwithstanding the above, nothing herein shall supersede or modify - the terms of any separate license agreement you may have executed - with Licensor regarding such Contributions. - - 6. Trademarks. This License does not grant permission to use the trade - names, trademarks, service marks, or product names of the Licensor, - except as required for reasonable and customary use in describing the - origin of the Work and reproducing the content of the NOTICE file. - - 7. Disclaimer of Warranty. Unless required by applicable law or - agreed to in writing, Licensor provides the Work (and each - Contributor provides its Contributions) on an "AS IS" BASIS, - WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or - implied, including, without limitation, any warranties or conditions - of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A - PARTICULAR PURPOSE. You are solely responsible for determining the - appropriateness of using or redistributing the Work and assume any - risks associated with Your exercise of permissions under this License. - - 8. Limitation of Liability. In no event and under no legal theory, - whether in tort (including negligence), contract, or otherwise, - unless required by applicable law (such as deliberate and grossly - negligent acts) or agreed to in writing, shall any Contributor be - liable to You for damages, including any direct, indirect, special, - incidental, or consequential damages of any character arising as a - result of this License or out of the use or inability to use the - Work (including but not limited to damages for loss of goodwill, - work stoppage, computer failure or malfunction, or any and all - other commercial damages or losses), even if such Contributor - has been advised of the possibility of such damages. - - 9. Accepting Warranty or Additional Liability. While redistributing - the Work or Derivative Works thereof, You may choose to offer, - and charge a fee for, acceptance of support, warranty, indemnity, - or other liability obligations and/or rights consistent with this - License. However, in accepting such obligations, You may act only - on Your own behalf and on Your sole responsibility, not on behalf - of any other Contributor, and only if You agree to indemnify, - defend, and hold each Contributor harmless for any liability - incurred by, or claims asserted against, such Contributor by reason - of your accepting any such warranty or additional liability. - - END OF TERMS AND CONDITIONS - - APPENDIX: How to apply the Apache License to your work. - - To apply the Apache License to your work, attach the following - boilerplate notice, with the fields enclosed by brackets "[]" - replaced with your own identifying information. (Don't include - the brackets!) The text should be enclosed in the appropriate - comment syntax for the file format. We also recommend that a - file or class name and description of purpose be included on the - same "printed page" as the copyright notice for easier - identification within third-party archives. - - Copyright [yyyy] [name of copyright owner] - - Licensed under the Apache License, Version 2.0 (the "License"); - you may not use this file except in compliance with the License. - You may obtain a copy of the License at - - http://www.apache.org/licenses/LICENSE-2.0 - - Unless required by applicable law or agreed to in writing, software - distributed under the License is distributed on an "AS IS" BASIS, - WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - See the License for the specific language governing permissions and - limitations under the License. + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS + + APPENDIX: How to apply the Apache License to your work. + + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "[]" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. We also recommend that a + file or class name and description of purpose be included on the + same "printed page" as the copyright notice for easier + identification within third-party archives. + + Copyright [yyyy] [name of copyright owner] + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. diff --git a/META-INF/MANIFEST.MF b/META-INF/MANIFEST.MF index 3314070..d000648 100644 --- a/META-INF/MANIFEST.MF +++ b/META-INF/MANIFEST.MF @@ -1,6 +1,6 @@ -Manifest-Version: 1.0 -Created-By: Maven JAR Plugin 3.4.1 -Build-Jdk-Spec: 21 -Main-Class: com.studentgui.app.Main -Built-By: mrhunsaker - +Manifest-Version: 1.0 +Created-By: Maven JAR Plugin 3.4.1 +Build-Jdk-Spec: 21 +Main-Class: com.studentgui.app.Main +Built-By: mrhunsaker + diff --git a/README.md b/README.md index df9bc92..77cdc0d 100644 --- a/README.md +++ b/README.md @@ -1,140 +1,265 @@ -# StudentGUI_java - -Vision Skills Progression Tracker (StudentGUI_java) - -This project is a Swing-based desktop application (Java 21, Maven) that -helps collect and visualize student assessment progress across a set of -skill-based progressions (Braille, Abacus, Keyboarding, ScreenReader, -DigitalLiteracy, iOS, CVI, and others). - -This repository contains the UI pages under `src/main/java/com/studentgui/apppages`, helper classes under -`src/main/java/com/studentgui/apphelpers`, and a small embedded SQLite-backed -database used to persist assessment sessions. - -Summary of recent changes (refactor/exception-cleanup branch) - -- Default student selection: UI pages now fall back to the first roster - entry when opened with a `null` or empty student name. The helper - `com.studentgui.apphelpers.Helpers.defaultStudent()` returns the first - entry from `json_Files/students.json` (or a sensible fallback). - -- Braille submit fix: `Braille.submitData()` now allocates the codes and - scores arrays dynamically using the actual `partCodes.length` so the - data written to the normalized schema matches what the plotting logic - expects. This removes earlier mismatches caused by fixed-size arrays. - -````markdown -# StudentGUI_java - -Vision Skills Progression Tracker (StudentGUI_java) - -This project is a Swing-based desktop application (Java 21, Maven) that -helps collect and visualize student assessment progress across a set of -skill-based progressions (Braille, Abacus, Keyboarding, ScreenReader, -DigitalLiteracy, iOS, CVI, and others). - -This repository contains the UI pages under `src/main/java/com/studentgui/apppages`, helper classes under -`src/main/java/com/studentgui/apphelpers`, and a small embedded SQLite-backed -database used to persist assessment sessions. - -Summary of recent changes (refactor/exception-cleanup branch) - -- Default student selection: UI pages now fall back to the first roster - entry when opened with a `null` or empty student name. The helper - `com.studentgui.apphelpers.Helpers.defaultStudent()` returns the first - entry from `json_Files/students.json` (or a sensible fallback). - -- Braille submit fix: `Braille.submitData()` now allocates the codes and - scores arrays dynamically using the actual `partCodes.length` so the - data written to the normalized schema matches what the plotting logic - expects. This removes earlier mismatches caused by fixed-size arrays. - -- Plotting improvements in `JLineGraph`: - - - Jitter: plotted points receive a small visual jitter of ±0.10 to - help reveal overlapping points. This is a rendering-only effect - and does not change stored database values. Jitter can be toggled - at runtime via the centralized Preferences dialog and can be made - deterministic/seeded for reproducible exported charts. - - - Background bands: horizontal colored bands have been added and the - Y-axis limits are set to `-0.25 .. 4.25` to show the bands and - provide visual breathing room. - -- Runtime preferences and centralization: - - - `PreferencesDialog` is the canonical runtime UI for plot and UI - settings. It persists `jitter.enabled`, `jitter.deterministic` and - `jitter.seed` to the app properties and calls - `Main.notifySettingsChanged()` so live components apply new settings. - - - `SettingsChangeListener` is used to notify live components; the - shared `JLineGraph` is registered at startup by `Main`. - -- UI theming: - - - Added a guarded "Material Theme UI Lite" submenu to the Themes menu - that exposes a curated list of Material themes when the - corresponding classes are present on the classpath. - - - Theme selection persists to app properties and is applied at - startup. - -How to build - -Open a PowerShell terminal and run the following (from the project root): - -```powershell -# compile and package (skip tests for a quicker cycle) -mvn -DskipTests package - -# the shaded jar will be produced in target/ (name includes the project id) -``` - -How to run the packaged jar - -After building, run the shaded jar with Java 21 (example PowerShell): - -```powershell -# replace the jar name with the artifact produced in target/ -java -jar .\\target\\vision-skills-progression-tracker-1.0.0-beta.jar -``` - -Where the app stores files and plots - -- Per-student plots are saved under `app_home/StudentDataFiles/{safeName}/plots`. -- Session markdown and HTML reports are written to `app_home/StudentDataFiles/{safeName}/reports`. -- `app_home` is created in the repository working directory by default; see `com.studentgui.apphelpers.Helpers.APP_HOME`. - -Configuration & toggles - -- To change the default roster used at startup, edit `json_Files/students.json`. - -- Runtime jitter controls: use the centralized `PreferencesDialog` (menu -> Themes/Preferences depending on platform) to enable/disable jitter at runtime, switch deterministic mode, and set a seed. The preferences are persisted under: - - - `jitter.enabled` = true/false - - `jitter.deterministic` = true/false - - `jitter.seed` = long integer (optional) - - These settings are applied immediately to the registered `JLineGraph` instance(s). - -Notes for maintainers - -- The `Helpers.defaultStudent()` method is used to provide a non-null - default student name when a page is constructed without a student. -- The Braille page previously used fixed-size arrays when creating the - assessment insert arrays which could be out-of-sync with the parts - list; that has been fixed so plotted columns match database columns. -- `JLineGraph` applies jitter only to displayed values (it never mutates - persisted session data). - -Testing & smoke utility - -- Unit tests: run `mvn test` — the branch's local run shows all unit - tests passing (11 tests, 0 failures). A smoke utility (`SmokeTest`) can - be executed to validate chart PNG export and confirms files are written - to `app_home/StudentDataFiles/Smoke Test/plots`. - -If you'd like I can add CI to run `mvn test` on PRs and make javadoc -warnings fail the build; say which option you prefer and I'll implement it. -```` \ No newline at end of file +# StudentGUI_java + +Vision Skills Progression Tracker (Swing desktop app — Java & Maven) + +This repository contains a Swing-based desktop application used to collect +and visualize student assessment progress across several skill-based +progressions (Braille, Abacus, Keyboarding, ScreenReader, DigitalLiteracy, +iOS, CVI, etc.). The code lives under `src/main/java/com/studentgui` and +the app stores per-student data under the `app_home/` folder (created on +first run). + +This README focuses on helping a non-expert user build the project and +produce a clickable desktop application (native installer or launcher) +for macOS, Windows 11, and common Linux distributions (APT/Pacman/DNF/RPM +families) so they can run the app by double-clicking an icon. + +## Quick summary + +- Build with Maven (JDK 21 recommended). +- Run the produced shaded (fat) jar with `java -jar`. +- For a truly "clickable" app on each platform, use the JDK `jpackage` + tool to produce a native installer or application bundle (examples below). + +## Prerequisites + +- Java Development Kit (JDK) 21 is recommended. If you cannot get 21, + JDK 17 will often work but some minor features may differ. +- Maven (to build the project). + +Install Java with your platform's package manager (examples): + +macOS (Homebrew): + +```bash +brew update +brew install temurin@21 # or `brew install openjdk` then follow brew notes +``` + +Windows 11 (winget): + +```powershell +winget install --id EclipseAdoptium.Temurin.21JDK -e +# Or download the MSI from Adoptium/Temurin or Microsoft Build of OpenJDK +``` + +Ubuntu / Debian (apt): + +```bash +sudo apt update +# If available on your distro: +sudo apt install openjdk-21-jdk +# Fallback (widely available): +sudo apt install openjdk-17-jdk +``` + +Arch Linux (pacman): + +```bash +sudo pacman -Syu +sudo pacman -S jdk-openjdk +``` + +Fedora / RHEL (dnf): + +```bash +sudo dnf install java-21-openjdk-devel +``` + +openSUSE (zypper / rpm-based): + +```bash +sudo zypper install java-21-openjdk-devel +``` + +If your distribution doesn't yet ship JDK 21, use the vendor installers +from Adoptium / Eclipse Temurin, or consider SDKMAN (cross-platform). + +Verify Java is available: + +```bash +java -version +mvn -v +``` + +## Build the project + +From the project root (where `pom.xml` is located): + +```bash +# Build and package (skip tests for a faster local build) +mvn -DskipTests package +``` + +This will produce a packaged jar under `target/`. The project produces a +shaded jar (fat jar) that contains dependencies — look for a file named +like `vision-skills-progression-tracker-.jar` or similar in +`target/`. + +## Run the jar directly (simple, cross-platform) + +If you just want to run the app without creating a platform installer, +double-clicking the jar is possible once Java is installed and the `.jar` +file type is associated with Java. Otherwise you can run from a terminal: + +Windows (PowerShell): + +```powershell +java -jar .\target\vision-skills-progression-tracker-1.0.0-beta.jar +``` + +macOS / Linux (bash/zsh/pwsh): + +```bash +java -jar target/vision-skills-progression-tracker-1.0.0-beta.jar +``` + +Note: replace the jar name above with the actual artifact that was +produced in `target/`. + +## Make a clickable native app (recommended) — using jpackage + +`jpackage` is included with modern JDKs and creates native installers or +application bundles for macOS, Windows, and many Linux distributions. It +wraps the JRE with your app so end users don't need to install Java. + +General tips before running `jpackage`: + +- Copy the produced jar into a small `app` folder or use the `target/` + directory as the input directory. +- If your fat jar contains a proper `Main-Class` in its manifest, + `jpackage` can automatically start the app. Otherwise pass + `--main-class` with the fully-qualified main class name. +- Provide an icon file for a nicer-looking app (`.icns` for macOS, + `.ico` for Windows, and `.png` for Linux). + +Example commands (replace placeholders with actual names): + +macOS (.dmg or .pkg): + +```bash +# Build first +mvn -DskipTests package + +# Create a macOS app bundle / dmg (run on macOS machine) +jpackage \ + --type dmg \ + --name "VisionSkillsTracker" \ + --input target \ + --main-jar vision-skills-progression-tracker-1.0.0-beta.jar \ + --icon path/to/icon.icns \ + --app-version 1.0.0 +``` + +Windows (.msi or .exe): + +```powershell +# Run on Windows 11 machine with JDK 21 installed +mvn -DskipTests package + +jpackage --type msi \ + --name "VisionSkillsTracker" \ + --input target \ + --main-jar vision-skills-progression-tracker-1.0.0-beta.jar \ + --icon path\to\icon.ico \ + --app-version 1.0.0 +``` + +Linux (.deb or .rpm): + +```bash +# On Debian/Ubuntu-based systems for a .deb package +jpackage --type deb \ + --name "VisionSkillsTracker" \ + --input target \ + --main-jar vision-skills-progression-tracker-1.0.0-beta.jar \ + --icon path/to/icon.png \ + --app-version 1.0.0 + +# For RPM-based systems, use --type rpm instead of deb +``` + +`jpackage` produces a native installer or application bundle which the +end-user can double-click like any other native app. This is the most +painless cross-platform approach for non-technical users. + +Important: `jpackage` must be run on the target platform to produce +platform-specific installers (run on mac to make a .dmg/.pkg, on +windows to make .msi/.exe, etc.). + +## Simpler alternatives (no installer) + +- Windows: create a small `run.bat` next to the jar which runs + `java -jar "vision-...jar"` and create a shortcut to the `.bat`. +- macOS: wrap the jar into an application with tools like `jar2app` + (3rd-party) or use `jpackage` as above. +- Linux: create a `.desktop` file that runs `java -jar /path/to/jar` + (make it executable and place in `~/.local/share/applications/` for + user-level apps). + +Example `.desktop` file (replace paths): + +```ini +[Desktop Entry] +Name=VisionSkillsTracker +Comment=Student GUI app +Exec=java -jar /path/to/vision-skills-progression-tracker-1.0.0-beta.jar +Terminal=false +Type=Application +Icon=/path/to/icon.png +Categories=Education;Utility; +``` + +Make it executable: + +```bash +chmod +x ~/.local/share/applications/vision-skills-tracker.desktop +``` + +## Troubleshooting & tips for non-experts + +- Double-clicking a `.jar` only works if Java is installed and the + file association is set. If double-click doesn't work, run from a + terminal so you can see error messages. +- macOS Gatekeeper: if macOS prevents the app from opening because it's + unsigned, right-click the app and choose Open, then confirm. +- Windows SmartScreen: similar warnings may appear for unsigned apps — + there will usually be an option to run anyway. +- If you get "no main manifest attribute", the jar's manifest doesn't + specify a Main-Class. Run with `java -cp jarname.jar com.studentgui.Main` + (replace with the real main class) or rebuild so the shaded jar has + the main class set. + +## Where the app stores files + +- Per-student plots: `app_home/StudentDataFiles/{safeName}/plots` +- Reports: `app_home/StudentDataFiles/{safeName}/reports` +- `app_home` is created in the working directory by default; the + helper constant `com.studentgui.apphelpers.Helpers.APP_HOME` controls + this behavior. + +## Developers / Maintainers notes + +- Run unit tests: `mvn test` +- Project entry points and runtime preferences are under + `src/main/java/com/studentgui` (look for `Main` and the pages under + `apppages`). + +--- + +## GitHub Actions (CI, Site, Packages) + +- `CI`: builds and runs tests on every push/PR. +- `Maven Site`: runs `mvn site` and uploads the generated site (`target/site`) as a workflow artifact. Trigger it via the "Maven Site" workflow (Actions tab → Run workflow). Download the artifact to view locally or host elsewhere. +- `Package Linux`: builds `.deb`, `.rpm`, and attempts an AppImage. Artifacts are uploaded from `dist/linux/`. +- `Package Windows`: builds a `.msi` via `jpackage`. Artifact is uploaded from `dist/windows/`. +- `Package macOS`: builds a `.dmg` via `jpackage`. Artifact is uploaded from `dist/macos/`. +- `Deploy Maven Site`: builds `mvn site` and publishes to GitHub Pages on `main`/`master` or manual run. +- `Release`: on Release publish, builds installers for Linux/Windows/macOS and attaches them to the GitHub Release automatically. + +Notes: +- Linux DMG is not a standard format; DMG is macOS-only. On Linux, prefer `.deb`, `.rpm`, or AppImage. +- These packaging workflows follow the templates under `examples/` and run `jpackage` on the appropriate OS runner to produce native installers. +- The displayed app name is taken from `pom.xml `; if absent, it falls back to `VisionSkillsTracker`. Icons are sourced from `examples/icons`. + + \ No newline at end of file diff --git a/REPORT-pages-db-methods.md b/REPORT-pages-db-methods.md index c2c800d..9ed07db 100644 --- a/REPORT-pages-db-methods.md +++ b/REPORT-pages-db-methods.md @@ -1,143 +1,143 @@ -# Pages and Database helper methods used - -This report lists each app page under src/main/java/com/studentgui/apppages and the Database helper methods (and other helpers) each page calls when saving or refreshing. - -- Abacus.java - - getOrCreateStudent(studentName) - - getOrCreateProgressType("Abacus") - - createProgressSession(studentId, ptId, date) - - insertAssessmentResults(sessionId, ptId, codes, scores) - - fetchLatestAssessmentResults(studentName, "Abacus", n) - - SessionJsonWriter.writeSessionJson(studentName, "Abacus", AssessmentPayload, sessionId) - - JLineGraph.saveChart(...) (saves PNG) - -- Braille.java - - getOrCreateStudent(studentName) - - getOrCreateProgressType("Braille") - - createProgressSession(studentId, ptId, date) - - insertAssessmentResults(sessionId, ptId, codes, scores) - - fetchLatestAssessmentResults(studentName, "Braille", n) - - SessionJsonWriter.writeSessionJson(..., "Braille", AssessmentPayload, sessionId) - - JLineGraph.saveChart(...) - -- BrailleNote.java - - getOrCreateStudent(studentName) - - getOrCreateProgressType("BrailleNote") - - createProgressSession(...) - - insertAssessmentResults(...) - - SessionJsonWriter.writeSessionJson(..., "BrailleNote", AssessmentPayload, sessionId) - - JLineGraph.saveChart(...) - -- BrailleSense.java - - getOrCreateStudent(studentName) - - getOrCreateProgressType("BrailleSense") - - createProgressSession(...) - - insertAssessmentResults(...) - - SessionJsonWriter.writeSessionJson(..., "BrailleSense", AssessmentPayload, sessionId) - - JLineGraph.saveChart(...) - -- ScreenReader.java - - getOrCreateStudent(studentName) - - getOrCreateProgressType("ScreenReader") - - createProgressSession(...) - - insertAssessmentResults(...) - - fetchLatestAssessmentResults(studentName, "ScreenReader", n) - - SessionJsonWriter.writeSessionJson(..., "ScreenReader", AssessmentPayload, sessionId) - - JLineGraph.saveChart(...) - -- DigitalLiteracy.java - - getOrCreateStudent(studentName) - - getOrCreateProgressType("DigitalLiteracy") - - createProgressSession(...) - - insertAssessmentResults(...) - - SessionJsonWriter.writeSessionJson(..., "DigitalLiteracy", AssessmentPayload, sessionId) - - JLineGraph.saveChart(...) - -- IOS.java - - getOrCreateStudent(studentName) - - getOrCreateProgressType("iOS") - - createProgressSession(...) - - insertAssessmentResults(...) - - fetchLatestAssessmentResults(studentName, "iOS", n) - - SessionJsonWriter.writeSessionJson(..., "iOS", AssessmentPayload, sessionId) - - JLineGraph.saveChart(...) - -- CVI.java - - getOrCreateStudent(studentName) - - getOrCreateProgressType("CVI") - - createProgressSession(...) - - insertAssessmentResults(...) - - SessionJsonWriter.writeSessionJson(..., "CVI", AssessmentPayload, sessionId) - - JLineGraph.saveChart(...) - -- Keyboarding.java (specialized keyboarding table) - - getOrCreateStudent(studentName) - - getOrCreateProgressType("Keyboarding") - - createProgressSession(...) - - insertKeyboardingResult(sessionId, program, topic, speed, accuracy) - - SessionJsonWriter.writeSessionJson(..., "Keyboarding", KeyboardingPayload, sessionId) - -- Observations.java - - getOrCreateStudent(studentName) - - getOrCreateProgressType("Observations") - - createProgressSession(...) - - insertAssessmentResults(sessionId, ptId, new String[]{"OBS_NOTE"}, new int[]{0}) - - saveSessionNotes(sessionId, notes) - - SessionJsonWriter.writeSessionJson(..., "Observations", NotesPayload, sessionId) - -- ContactLog.java - - getOrCreateStudent(studentName) - - getOrCreateProgressType("ContactLog") - - createProgressSession(...) - - saveSessionNotes(sessionId, notes) - - saveContactLog(sessionId, studentName, dateString, guardian, method, phone, email, response, general, specific, notes) - - SessionJsonWriter.writeSessionJson(..., "ContactLog", ContactPayload, sessionId) - - fetchLatestContactLog(studentName) used on Load Last Contact - -- SessionNotes.java - - getOrCreateStudent(studentName) - - getOrCreateProgressType("SessionNotes") - - createProgressSession(...) - - saveSessionNotes(sessionId, notes) - - SessionJsonWriter.writeSessionJson(..., "SessionNotes", NotesPayload, sessionId) - -- InstructionalMaterials.java - - No DB persistence (static read-only viewer) - -- Homepage (Homepage.create()) - - No DB persistence (static overview pane) - -Notes - -- All assessment pages that create assessment sessions call SessionJsonWriter.writeSessionJson(...) with a typed DTO (AssessmentPayload, NotesPayload, KeyboardingPayload, ContactPayload, etc.). -- The SQL schema generator (SqlGenerate) creates the tables referenced above: Student, ProgressType, ProgressSession, AssessmentPart, AssessmentResult, KeyboardingResult, ContactLog, etc. - -Recent changes (applied in branch refactor/exception-cleanup) - -- Braille submitData array sizing - - The `Braille` page previously constructed fixed-size arrays when preparing - the `codes` and `scores` to persist into the normalized schema. That could - lead to a mismatch between stored columns and the plotted series. The code - now allocates arrays using the actual `partCodes.length` so storage and - plotting stay aligned. - -- Default student behavior - - Many pages now default to the first roster entry when constructed with a - null or empty student name. The helper `com.studentgui.apphelpers.Helpers.defaultStudent()` - returns the first entry in `json_Files/students.json` (or a sensible - fallback) and is used by pages to avoid null student names on open. - -- Plot visualization updates (JLineGraph) - - Plotted points receive a small rendering jitter of ±0.10 so overlapping - points are easier to identify visually. This jitter is applied at render - time only and does not mutate persisted values. - - Background bands have been changed to the following numeric ranges: - red = -0.25..0.5, orange = 0.5..1.5, orange = 1.5..2.5, yellow = 2.5..3.5, - green = 3.5..4.5. The Y-axis limits have been adjusted to -0.25..4.25. - -Verification & build notes - -- After applying these changes, run `mvn -DskipTests package` in the project - root to compile the project and produce the shaded jar in `target/`. - -End of report. +# Pages and Database helper methods used + +This report lists each app page under src/main/java/com/studentgui/apppages and the Database helper methods (and other helpers) each page calls when saving or refreshing. + +- Abacus.java + - getOrCreateStudent(studentName) + - getOrCreateProgressType("Abacus") + - createProgressSession(studentId, ptId, date) + - insertAssessmentResults(sessionId, ptId, codes, scores) + - fetchLatestAssessmentResults(studentName, "Abacus", n) + - SessionJsonWriter.writeSessionJson(studentName, "Abacus", AssessmentPayload, sessionId) + - JLineGraph.saveChart(...) (saves PNG) + +- Braille.java + - getOrCreateStudent(studentName) + - getOrCreateProgressType("Braille") + - createProgressSession(studentId, ptId, date) + - insertAssessmentResults(sessionId, ptId, codes, scores) + - fetchLatestAssessmentResults(studentName, "Braille", n) + - SessionJsonWriter.writeSessionJson(..., "Braille", AssessmentPayload, sessionId) + - JLineGraph.saveChart(...) + +- BrailleNote.java + - getOrCreateStudent(studentName) + - getOrCreateProgressType("BrailleNote") + - createProgressSession(...) + - insertAssessmentResults(...) + - SessionJsonWriter.writeSessionJson(..., "BrailleNote", AssessmentPayload, sessionId) + - JLineGraph.saveChart(...) + +- BrailleSense.java + - getOrCreateStudent(studentName) + - getOrCreateProgressType("BrailleSense") + - createProgressSession(...) + - insertAssessmentResults(...) + - SessionJsonWriter.writeSessionJson(..., "BrailleSense", AssessmentPayload, sessionId) + - JLineGraph.saveChart(...) + +- ScreenReader.java + - getOrCreateStudent(studentName) + - getOrCreateProgressType("ScreenReader") + - createProgressSession(...) + - insertAssessmentResults(...) + - fetchLatestAssessmentResults(studentName, "ScreenReader", n) + - SessionJsonWriter.writeSessionJson(..., "ScreenReader", AssessmentPayload, sessionId) + - JLineGraph.saveChart(...) + +- DigitalLiteracy.java + - getOrCreateStudent(studentName) + - getOrCreateProgressType("DigitalLiteracy") + - createProgressSession(...) + - insertAssessmentResults(...) + - SessionJsonWriter.writeSessionJson(..., "DigitalLiteracy", AssessmentPayload, sessionId) + - JLineGraph.saveChart(...) + +- IOS.java + - getOrCreateStudent(studentName) + - getOrCreateProgressType("iOS") + - createProgressSession(...) + - insertAssessmentResults(...) + - fetchLatestAssessmentResults(studentName, "iOS", n) + - SessionJsonWriter.writeSessionJson(..., "iOS", AssessmentPayload, sessionId) + - JLineGraph.saveChart(...) + +- CVI.java + - getOrCreateStudent(studentName) + - getOrCreateProgressType("CVI") + - createProgressSession(...) + - insertAssessmentResults(...) + - SessionJsonWriter.writeSessionJson(..., "CVI", AssessmentPayload, sessionId) + - JLineGraph.saveChart(...) + +- Keyboarding.java (specialized keyboarding table) + - getOrCreateStudent(studentName) + - getOrCreateProgressType("Keyboarding") + - createProgressSession(...) + - insertKeyboardingResult(sessionId, program, topic, speed, accuracy) + - SessionJsonWriter.writeSessionJson(..., "Keyboarding", KeyboardingPayload, sessionId) + +- Observations.java + - getOrCreateStudent(studentName) + - getOrCreateProgressType("Observations") + - createProgressSession(...) + - insertAssessmentResults(sessionId, ptId, new String[]{"OBS_NOTE"}, new int[]{0}) + - saveSessionNotes(sessionId, notes) + - SessionJsonWriter.writeSessionJson(..., "Observations", NotesPayload, sessionId) + +- ContactLog.java + - getOrCreateStudent(studentName) + - getOrCreateProgressType("ContactLog") + - createProgressSession(...) + - saveSessionNotes(sessionId, notes) + - saveContactLog(sessionId, studentName, dateString, guardian, method, phone, email, response, general, specific, notes) + - SessionJsonWriter.writeSessionJson(..., "ContactLog", ContactPayload, sessionId) + - fetchLatestContactLog(studentName) used on Load Last Contact + +- SessionNotes.java + - getOrCreateStudent(studentName) + - getOrCreateProgressType("SessionNotes") + - createProgressSession(...) + - saveSessionNotes(sessionId, notes) + - SessionJsonWriter.writeSessionJson(..., "SessionNotes", NotesPayload, sessionId) + +- InstructionalMaterials.java + - No DB persistence (static read-only viewer) + +- Homepage (Homepage.create()) + - No DB persistence (static overview pane) + +Notes + +- All assessment pages that create assessment sessions call SessionJsonWriter.writeSessionJson(...) with a typed DTO (AssessmentPayload, NotesPayload, KeyboardingPayload, ContactPayload, etc.). +- The SQL schema generator (SqlGenerate) creates the tables referenced above: Student, ProgressType, ProgressSession, AssessmentPart, AssessmentResult, KeyboardingResult, ContactLog, etc. + +Recent changes (applied in branch refactor/exception-cleanup) + +- Braille submitData array sizing + - The `Braille` page previously constructed fixed-size arrays when preparing + the `codes` and `scores` to persist into the normalized schema. That could + lead to a mismatch between stored columns and the plotted series. The code + now allocates arrays using the actual `partCodes.length` so storage and + plotting stay aligned. + +- Default student behavior + - Many pages now default to the first roster entry when constructed with a + null or empty student name. The helper `com.studentgui.apphelpers.Helpers.defaultStudent()` + returns the first entry in `json_Files/students.json` (or a sensible + fallback) and is used by pages to avoid null student names on open. + +- Plot visualization updates (JLineGraph) + - Plotted points receive a small rendering jitter of ±0.10 so overlapping + points are easier to identify visually. This jitter is applied at render + time only and does not mutate persisted values. + - Background bands have been changed to the following numeric ranges: + red = -0.25..0.5, orange = 0.5..1.5, orange = 1.5..2.5, yellow = 2.5..3.5, + green = 3.5..4.5. The Y-axis limits have been adjusted to -0.25..4.25. + +Verification & build notes + +- After applying these changes, run `mvn -DskipTests package` in the project + root to compile the project and produce the shaded jar in `target/`. + +End of report. diff --git a/config/checkstyle/checkstyle.xml b/config/checkstyle/checkstyle.xml index 65ffe06..7ca6238 100644 --- a/config/checkstyle/checkstyle.xml +++ b/config/checkstyle/checkstyle.xml @@ -1,29 +1,29 @@ - - - - - - - - - - - - - - - - - - - - - - - - + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/examples/.github/workflows/ci-actionlint.yml b/examples/.github/workflows/ci-actionlint.yml new file mode 100644 index 0000000..8d79c6b --- /dev/null +++ b/examples/.github/workflows/ci-actionlint.yml @@ -0,0 +1,39 @@ +name: Lint GitHub Workflows + +on: + pull_request: + paths: + - '.github/workflows/**' + push: + branches: [ main ] + paths: + - '.github/workflows/**' + +permissions: + contents: read + +jobs: + actionlint: + name: actionlint + runs-on: ubuntu-latest + steps: + - name: Checkout + uses: actions/checkout@v4 + + - name: Install ShellCheck (for shell script checks) + run: | + sudo apt-get update + sudo apt-get install -y --no-install-recommends shellcheck + + - name: Install actionlint + run: | + set -euxo pipefail + tmpdir=$(mktemp -d) + curl -fsSL https://raw.githubusercontent.com/rhysd/actionlint/main/scripts/download-actionlint.bash -o "$tmpdir/download.sh" + bash "$tmpdir/download.sh" -b "$tmpdir" + sudo mv "$tmpdir"/actionlint /usr/local/bin/actionlint + actionlint -version + + - name: Run actionlint + run: | + actionlint -color diff --git a/examples/.github/workflows/ci-maven.yml b/examples/.github/workflows/ci-maven.yml new file mode 100644 index 0000000..2de2226 --- /dev/null +++ b/examples/.github/workflows/ci-maven.yml @@ -0,0 +1,30 @@ +name: PR Build (Maven) + +on: + pull_request: + push: + branches: [ main ] + +permissions: + contents: read + +concurrency: + group: pr-build-${{ github.ref }} + cancel-in-progress: true + +jobs: + build: + runs-on: ubuntu-latest + steps: + - name: Checkout + uses: actions/checkout@v4 + + - name: Set up Temurin JDK 21 + uses: actions/setup-java@v4 + with: + distribution: temurin + java-version: '21' + cache: maven + + - name: Maven verify (skip tests) + run: mvn -B -ntp -q -DskipTests verify diff --git a/examples/.github/workflows/deploy-javadocs.yml b/examples/.github/workflows/deploy-javadocs.yml new file mode 100644 index 0000000..6378e6e --- /dev/null +++ b/examples/.github/workflows/deploy-javadocs.yml @@ -0,0 +1,60 @@ +name: Deploy API Docs (Javadocs Only) + +on: + push: + branches: [main] + workflow_dispatch: + +jobs: + build-and-deploy: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + + - name: Set up JDK + uses: actions/setup-java@v4 + with: + distribution: temurin + java-version: "21" + + - name: Cache Maven repository + uses: actions/cache@v4 + with: + path: ~/.m2/repository + key: ${{ runner.os }}-m2-${{ hashFiles('**/pom.xml') }} + restore-keys: ${{ runner.os }}-m2- + + - name: Build Maven site and apidocs + run: | + mvn -B -ntp -DskipTests site + # Ensure apidocs are present; if not, generate explicitly + if [ ! -d target/site/apidocs ]; then + echo "apidocs not found after 'mvn site'; generating explicitly via javadoc:javadoc" >&2 + mvn -B -ntp -DskipTests javadoc:javadoc + fi + test -d target/site/apidocs || { echo "ERROR: target/site/apidocs still missing after generation" >&2; exit 1; } + + - name: Determine project version + id: get-version + run: echo "version=$(mvn -q -DforceStdout help:evaluate -Dexpression=project.version)" >> $GITHUB_OUTPUT + + - name: Prepare versioned Javadocs + run: | + mkdir -p build/site-workdir + cp -R target/site/apidocs build/site-workdir/apidocs-temp + chmod +x scripts/generate-javadoc-index.sh + scripts/generate-javadoc-index.sh \ + --root build/site-workdir \ + --version "${{ steps.get-version.outputs.version }}" \ + --latest \ + --project-url "https://github.com/${{ github.repository }}" \ + --package com.digitizer.ui + + - name: Deploy versioned Javadocs to GitHub Pages + uses: peaceiris/actions-gh-pages@v3 + with: + github_token: ${{ secrets.GITHUB_TOKEN }} + publish_dir: build/site-workdir + publish_branch: gh-pages + force_orphan: true + disable_nojekyll: true diff --git a/examples/.github/workflows/release-packages.yml b/examples/.github/workflows/release-packages.yml new file mode 100644 index 0000000..c3e267e --- /dev/null +++ b/examples/.github/workflows/release-packages.yml @@ -0,0 +1,107 @@ +name: Build and Release Packages + +on: + push: + tags: + - "v*" + - "V*" + +permissions: + contents: write + +jobs: + linux-packages: + name: Linux Packages (AppImage, DEB, RPM) + runs-on: ubuntu-latest + env: + APP_NAME: GraphDigitizer + steps: + - name: Checkout + uses: actions/checkout@v4 + + - name: Set up Temurin JDK 21 + uses: actions/setup-java@v4 + with: + distribution: temurin + java-version: '21' + cache: maven + + - name: Install packaging dependencies (deb/rpm/fpm) + run: | + sudo apt-get update + sudo apt-get install -y --no-install-recommends \ + fakeroot rpm ruby ruby-dev rubygems build-essential xz-utils zsync wget + sudo gem install --no-document fpm + + - name: Install appimagetool + run: | + set -euxo pipefail + wget -O appimagetool.AppImage https://github.com/AppImage/AppImageKit/releases/download/continuous/appimagetool-x86_64.AppImage + chmod +x appimagetool.AppImage + ./appimagetool.AppImage --appimage-extract + sudo mv squashfs-root/AppRun /usr/local/bin/appimagetool + sudo chmod +x /usr/local/bin/appimagetool + + - name: Build with Maven + run: mvn -B -ntp -DskipTests package + + - name: Derive version from tag + run: echo "VERSION=${GITHUB_REF_NAME#v}" >> "$GITHUB_ENV" + + - name: Ensure scripts executable + run: chmod +x scripts/*.sh + + - name: Generate DEB + run: ./scripts/generate-deb.sh "$VERSION" + + - name: Generate RPM + run: ./scripts/generate-rpm.sh "$VERSION" + + - name: Generate AppImage + run: ./scripts/generate-appimage.sh "$VERSION" + + - name: Upload Linux artifacts to Release + uses: softprops/action-gh-release@v2 + if: startsWith(github.ref, 'refs/tags/') + with: + files: | + target/generated_builds/**/*.deb + target/generated_builds/**/*.rpm + target/generated_builds/**/*.AppImage + fail_on_unmatched_files: false + + macos-dmg: + name: macOS DMG + runs-on: macos-latest + env: + APP_NAME: GraphDigitizer + steps: + - name: Checkout + uses: actions/checkout@v4 + + - name: Set up Temurin JDK 21 + uses: actions/setup-java@v4 + with: + distribution: temurin + java-version: '21' + cache: maven + + - name: Build with Maven + run: mvn -B -ntp -DskipTests package + + - name: Derive version from tag + run: echo "VERSION=${GITHUB_REF_NAME#v}" >> "$GITHUB_ENV" + + - name: Ensure scripts executable + run: chmod +x scripts/*.sh + + - name: Generate DMG + run: ./scripts/generate-dmg.sh "$VERSION" + + - name: Upload macOS artifacts to Release + uses: softprops/action-gh-release@v2 + if: startsWith(github.ref, 'refs/tags/') + with: + files: | + target/generated_builds/**/*.dmg + fail_on_unmatched_files: false diff --git a/examples/.gitignore b/examples/.gitignore new file mode 100644 index 0000000..37bd144 --- /dev/null +++ b/examples/.gitignore @@ -0,0 +1,58 @@ +# Maven +target/ +*.jar +*.war +*.ear +pom.xml.tag +pom.xml.releaseBackup +pom.xml.versionsBackup +pom.xml.next +release.properties +dependency-reduced-pom.xml +GraphDigitizer/ + +# IDE +.idea/ +*.iml +*.iws +*.ipr +.vscode/ +*.code-workspace +.classpath +.project +.settings/ + +# OS +.DS_Store +Thumbs.db +*.swp +*.swo +*~ + +# Logs +logs/ +*.log + +# Build +build/ +out/ +bin/ + +# Runtime +*.class +*.ctxt +.mtj.tmp/ + +# Package Files +*.jar +*.war +*.nar +*.ear +*.zip +*.tar.gz +*.rar +*.exe +*.msi +*.dmp +*.bak +core.20251202.120453.9872.0001.dmp \ No newline at end of file diff --git a/examples/.gitmodules b/examples/.gitmodules new file mode 100644 index 0000000..d92562b --- /dev/null +++ b/examples/.gitmodules @@ -0,0 +1,3 @@ +[submodule "Julia_version"] + path = Julia_version + url = https://github.com/mrhunsaker/Graph_Digitizer.git diff --git a/examples/icons/scatter-plot-128.ico b/examples/icons/scatter-plot-128.ico new file mode 100644 index 0000000000000000000000000000000000000000..37d8c35f3c372369ee51ea1a1e8c4477cd43c3a7 GIT binary patch literal 67646 zcmeI5e~1><8pp?0R3cPFL_~B65%Ee$L`Xy{5+M@(5fTv*5fKq`kq{9HeG!omi3kY^ ziMWV_#3e!`L}YJ>M1(|$h%E8q6%h&98zM_AzW4Ke_nqA{hW?pypowN0I<~ip% z&-0w;d(N4eGqV~5^Z2jT3ixw4*sy3`up$V84S*sQcFT2|^)vR5&M^2AT=W>|9DEPI zo6NC*{0jIBbUYf71981gV}RotaL@DJ2;R;4eFx5$fiFDwjpIHX2YcZDYtMgU`9J6R zZ!Gs(1I~K>8_oZRp8v-3e--W*dHx&8{}{Zl>plP3-2Y9Ye*zZSsHw7FCe z7id=my64e#uPC&C@|^?DpnJ*!-=K3c1KOKAo`BCOJJ!Qdj)!|_aXbL_fxTcCcn`b= z7Ki&koZIj}f!?aHjXw@f20Z_R!)1}4c>arIZE)In{s)K4B0cf^7s=Y-wDJ584wprG z;`uLKLP#;#IebF+PTJo-^VtFF8P>~{1f+l zp!Xs1`JTpoSUyIZr;QXe`jlMERsQ!Ad&sHl@hWZoeE}oif&b-onJC2k40r%sc{lZs z)ulhP8OI#u|J#z>YaHAMD`zwJb?`mc_!noOs^3-FT?Y5G3i|2f7D|A zCk}nTk`!Z+vU9VU`#Sh0m!~cASttJYS;T8Q7RKtp!EDCxxcJAfI6)w{{FwB z!AUMF=O3G!OLBCC_NhIjPT$JM*AG`&W2hF-G^rhZ3>D;{QvR{0efdkkl@C*2*MjHL zx=-EH!1kSwY1cg8pJu;F<>awy{$IhS>pK~fm+<^d77zG%7yJ!EeV5|}+NEA&viuHO z9eJpl|0>pIajE&eEG^&E>D|R%ptZtAWH2)LJ0C}WEW&&&NU5nsuA{Pr#G?jAgcL)33_-S0Au< z5tQX!?X~9g?OQ!;Zv%ILZLGheyK%O7>X%>oR5kw}JLEJg_djFjiGK7OyZRPb<15R5 zQk}-jGyRBZwrtX$s`=NNnzf!Lji!*HMdYQzj zZP^vt_vLzv-7_U)Qy&hM9h=x#4Bi4?0qsX!0zZJm;3bfI-&!_?9#{R~U-yCPlP%w- zp5|s>)>!Q7nc(k&ahWmR#YUkufOb!Uy&&t}W0HE!f$g9Qnp16HzjFS2<4Vt7uJ?j^ zcF5fGN$rS51-AD9lgBdWuUWW^sNh@I#-AKw&{+Kp^ldG`c3=2c$FZ$-K+n{9djjg` zDX=<{u!;rjSF!eoW9Czr@bInqpl67%z02_g?fP{u+&BKi{1E$Uun%ZGXoK5e4EzXM zU;_x%`*y^~CE$|xEZtMKf#UNCP-g7udhoujKNmgh_MQJQ9(-v{SkOb?*0j)H{D|Mn zAd6F%Iv4(5#;-VZu^-pfoT>c16*eCm|8YJZw@hEl%)vMxM{VQB@An|yzKFcb)~Br= zaeZeIoAyfP{608+sPg{r4VxU}a~aUP>>_-}+g!Bq>7qki|F(!t{P*Q~+$Gksy7XZk zsN(rg&!|>wF?uh6IFChTT>}=C&4bwo{2axq_F7Cbn`e!wre`u#`z&Vh`t8HE7dqBihYyOE(d&pNx z=I^64;;<`zHh~uS7HG}THRc$23s_(4GkjFB|D*M%@@H^t)}8;|#ArJ|()8|Oqtg54 zG_I4%@vZ&eZ%Xt->yz`hkV&z`%dlHp{)ue?(0zRxuP)^u#QXj@W%{rfTrSv`!?f#5 zxY(^T|GMsHU7KC_Ki`)GIvAxdOM&i3Y~!9lcP~i1e{pb~s*3G8^S{+0S9v+r{bS$q zn3~{Ng<=42gXdgNu897SJT~>J|?;MFq^LRB_2?}vSJB@=E zf$lp`1Mz(ldkw`70~{l>31Ai&v%;BmF~@$ zJoW3GnCr~{7UDB=UFLiYTh8l0ZI%PgO&j0W(A(B8uKHn@c%d&cpxpxD7Ouo>t+WF07UFOy)q*8D48 z`l`KHo17=<{6*W1@qTB?=nsPjC47c)Sbryw6c7G&{W%15UoiKLOb*MOzZJ%AbHs0}gzsNy>-3!_`g4l^ zFo(pw8oUbL2fM&KU<+{Se`qf}(q8XQS0FFPHubvyx4Iso_d!KI71y`1E~Sd-dj4bCAt$;H zYwp@`oTYOjW;#{G*ZvCpg|YmKtyEuT1$v%;Ga;NSci=P#^I7Dm>+rgm=~NNjR!M(m zjK5*aYQGab&wm0dQkPt#?Jrv&>&Y!f!;}P2U|gv?_#wE=zV1#A6fMu#beHr@paC^pXWa-i51qth4!*- zt~E(x;0M||eHVbf=f6V!%9?@?U2Bd2*D-K`HqO5rP8-jES<)(Q4=1`V>v`Z$kTlQD zvIppV4y>>FX1KKT{F@1>(K+0#0H1*C;6Bjvf$leT4|)Tf0HuCMg!Z2Q3OOgATE91g z-9Yp5Bsd22T&QKb2!Wu9PmV!#6#*XK|f=r4JunqhKG)K)fIy~1k{W8$A zLj_*Tn;;I)f5|kGLw&QQd$u&*L;k~~^t3k2`tJ)7gAM<8OQvXGo0C7S&0m0!TbJY4 zw42p2fPWkQZw)MgB|Ap`-UC|KU3fQj_mvExRCCXN@$`|q<=}1!?llgy-#1)5igGRS z?dN~N6q38Wz?{oP&yN-ip>#9Pf3XyjH(mFCu84m<%RBAc7K{4PR`~Vv|IxJMmXJ3+ zgID#wR(70!cOy4Q3kA==MUoO!$lC@`biGe%=k$9*2_Y3K^86P{oA!G2ZBP=AMa!K2 zURg70;?T9|IdB-92UmOekz)&N24<7#bISetKb;Nx6Y{nalzHwm$BFiX3a$UN6Sv}A zHcXr5x$5+8czwKwlzIN+NwJX0oA&(1fSKo_=WXmb?E|5&HRxTS=SN$=`aUi4@0KCk z@IPjelE$f!JMH1x<~~c$`g_eV$@qO6WX<_Be-CK4!?5G|50l||B#*iWR6f&kZBwp$ zf2Vi2=&uLkCH(5Ug6{Q(qvzj|cT=7G>AjB)&uKcxX>Xd$JxAZ>o82^jVfiUJ!j|Vh zH{QC1y0Wnfc>dGrwW=Y9PXX6&x0QG6-_s5Q zq0MfP#AlJR53%L>50hKsNFMb(@IJT+vbeZK{Xc=z{eBo4?M~;|&+;9wJAp0Ff0*0~ zj^tBo!J9zu_RoXMy`yB6JFWNekZ3;&;yf0Wbsu5F|Fw7=UY?x{z}LkJ`PW)v!~fOU z>8n+2V?j4G*_wjbyJpc9PJ02Z;4TA?2jDb7YSOEO`A5U`4 zWjNElWPC2W%6rsJ&wtIyP6!XqRsuaox^f=s|4!TP?~8G?ng72ggyD;3F$3_n1>6H6 zr?y9}`y0a0IrsdB$(UnDxYN7;>%f-#Yv@jMukIq-@c+vkOHE_Kg+J{#={YXzJ9N$Y z{h+(%o5`WfhW|@u8-HHE48Wtt!4A*@C%~8B0N4%|rA17g=RYku4Jfzd|6&7D(a0`Z z@_(U`$!S2RE%`s&fK)WHiHH zwCf8;qw-KID$k3GL0pbiKO)BUj5EzR=JgC!$%*9s2)r$c;ed;VHouQTxmUAe74yJvwkiIG1Hxt9+WcI!Jz>2!U21j&$F zQwg804;GsBWOZstbnA&H9Kt>M;G~9}{vV;)XwaEJeu&VO+w;2e1YvaLiJ`7MPExuu zO{5=}+r(hjKPnGR*UNvK7(;!mP%W%_U0@3hxOWnwhrLnP*z_^0Gv|0M7; z-Tx`E(`ohcGgUjHLl`gCy9u9>N4smpjNB5vhJIWQx>IUKe`=6S%MXjl?Ss+wt+i1( zSQ3$0TBcK~Pj_^6M>iyY|> pa>wYe#7=8uCO*mkGx(8g@>Px88kw>a{@FSho=qJ`qv9jd{{hRsvE={& literal 0 HcmV?d00001 diff --git a/examples/icons/scatter-plot-128.png b/examples/icons/scatter-plot-128.png new file mode 100644 index 0000000000000000000000000000000000000000..cf08e1d7020285ff8ef7b29b38a667c065747894 GIT binary patch literal 3600 zcmV+r4)5`aP)zln_b?A(Rk8 z$?cu~@y+6T*}eD7oY}j3?#?gSkZ{k;ncef9nQy+=BvfIPo&d}M4gs$K8bxF_z@p|_yALY7mU)Q26R-B0i(kxeG+hW zAJ-fWTsJ`Di3~6uz#Kl93FaqRwTzjM8@lzf7|9K=mJ@4p3r@(vyI<5G!uIhYv6kSOUCd{XK_?54NNjrKbQd z0OK8+O#oi37J|49EFnheqk$)Y;U3LL0*@J`KMHXMC@Z%DMI3a;0@p(nfYM=<9t&I? zpsRUC=^;@7%E!5cS&~Nrr$hlLACm+2^@X?%EFU3*bH+tOP++WKW+pHl7>&5n_XFF3 z=YiEpmhLNXi;yp4S2P6&!p2q31{NU?z815A9Y*Qffn`aS9`d-=4zB^$W>=ir17P3! zmB4SP|L`k}0`3N$HcIzpy{pG-9&PIJb`*eu7^S;_M}Z3*o6Z1M8Ks9gwp)#a?HA<` z@Jtkd;w%EbPZ1YnzM!z4>L8l|TLCjjRIC-Z@|a66LUxr#c8zEJ@D@mYWSqQ|#klBGSsoA%>G zTmt;rNtpdOwx3;r`f?%)fIs_yje+{wMpzSXZlVBqVyE3F0(JLPlBJ=Slw3psD8}!A z-39yDkJM*q3Q+(GkYwp@#46{lzyV8=EZrJrMDE1Yu;Q!$?k&*vu3#haAVvfg#OeZb zf!oNxHS7lN0j~9o2?$1fumP|N+(IM@=q2h4LJwk?nFV~m*$*G0At)Hu%C_Dpy#bgA zOanfFgoNz{{)q_3W;<)-#wgu^SRT8Omv%3(H$(})C;)-64|gL0Yy?K>kx0_#Wk{q^ zCn5j`kc7|mzP_T57;<`+;mdRh|zY~w`?4eT6GX{xo!<% z$~8)lN8Wzl7jq_}Gvu~QbP~>^HE6NZ@(d&l>l9@Dt}|$2$}Rb zq5n&SxCGR|PT_asAN1J)EHO&EnCd>_d9PXw&pQJ}0jSMMz}E`)F$|dJaQ%M^-mjW& za_;V;0MzDM!u&SpR*Es@plD!8H9D`KAPPV)?DT(9pzcPJe?9KYi|ER_ycb3RIFb&I z-0BtSYh2E2Hv#Jd+;boBWZsKQtKHlE`6MKvq6_)rTYz_boFcsl`Mcy=0+KAnD18&+ zMlP})ywirN<)U-~V3a-$xEU#o-1~>>0rnWBR{*!W{1CkeX&ucyttVOfrcwGf;3uB< zdK37$OM}uZ0Y>Q|wzSGR;7n@2C6&O58-cft($hR{)k9c?Y>VUdi}8yw+ys2pb@nTT z09XBkgb@2oW%SYwoS%HXxy^9GkTeXtX7I&;xFmA;FljOVEV&;vXUe7=(M?LsL8 z0Jt5$eR&tK(kOk5W4rqiPhU|UPO{V`eH)Odq9jX~0iOhZiU?8fef?hG5#V&-Lf_>s zF-orj&gIOoq=^@by6 z=^;pkk037SGAMydDDEVgia2!EI<#4g1X)b+xZfe*st^w!4#yepv7eyiUIIKSSQN${ zS$kTb?k;h;*dDQRKJcF7eGUP0k}Q4Bv0YPWbsb;~Irq^ynCj}HO|o<+vc~3pn45tw zCRw`Nb#Tq1)de8riKV0Q_`X)TnuQc;UJh*Sb;&+tX}JtIKFQJ$*1Q3u)pdZ70_wel z*{$L0_EU)q9EuoucKRe}*x6_*a265`G!#)B-vFNT4J~t})de7=MQ%5N0$`IY-HU9) zoUsr78;CWt)_Ffy0!xk3Yk`}KBz@$e)t7)>go|3<=$euh8as2WLX28{9rjcyLHC+b zdcH?^S#DZg05nIgxxPFWs2?wEP2e@S1%3`ej9`mB15Nv-r3Jud&n^wnRmE!76zm(k z6hE5+zz=aRMxBdR*8u>Q0axH&Pq&(z>K)Qx->hSS3z6WBp-A%OHsnyowMmv1HNcz! zTw0)g2XLoRdaaLXxi4B?0Fo>{Xq3JLc#*zO$E?RQh!7ot&BVDAiTAHJ8y+-DF9#O; zSoW-d{h*Kj^D+iGEi9;wp{19A3Q3l3LiXaWTcg!TZQLW2Kcn;o$QwSBzJG@<;5)z@ zMrof=v5`oSM+nXj)X!l`ApihLmc9yn7IA&{=LFf0ymhl`otkYQ{z}{oO=Ey{M(L9s z+5j9wxMJHAL!0DVehH|?8b>a)=jL97)W@xO$POZ=*cX5Ytr_fJcqe zCpzc_BR%d@j}eGzJeYl?R02@J&IYe~HvbvLf+@=nH*t9$^G|I1-nbhs_i_-sMAm)zWE-&_R3E z2MBrW>Syv?WOaoNf$kZqHt}HwbXvE2;))jA(}V)a{hjIOf=f2Cguv8yk;Tx=tYDy3xe(hd9)i7{-*?9~tfs5(w{nq$*ZNHWL7;DZvL zB1{JEZafsbBujT7La>0okBF&M8=VErZKST$C>`KN!~^ETaNrL{>CXj7^864x#qe}wIqX8hzTZKPnSW&nqsNg)XETc0;i7`=3TqvHObf z-HAl-PPTCggNo2Pz!1WfR{;-Kstvw83i)o8gB5BjQ&uaVV=Hr<2^`M{7QTN0e*-pz z4#R3C@(Ms+Z(t8$k5tXI_q8+6dB9ajj6lUxcM#ZSlwJ+opJZvxTzwno|G53M6)rPC zo@);x>uLxk5c;v)S>M8$NL*hn+d*YX8V`KiZoO_E%x|c?Vzqpr;YLfQJa)s>2XLcUtcBm48&U5_B6uK(v2LfP-LCG3n_V4k3|&UZ7j47tG1>g&LEr7dIf&g zwnuC}@~FILXo{kwk{Pe)B^}k;5zcIZ<;8;R;8b6{w%m z($IjABum!fpj8Ozka4Z!JLo85 zUcl=V+hSJpe4u_xOX;$GY|`iI09_rRiB&^dh0+OtWeLTLIWMc1LEKKb2tdo&&TUlA z;sRZ40&WR3oKjQP834q#a4+sk?E6UJh(QzASA^0EK-IwtbIG55yBaAQ|9-;;Rx(N} z0C4zVg?oVGkPMow962NGLmvD&z?W?pSj1BLq7MM}sFb-z=>8N59`Xg<^w)2_aaXsizE?N!zp8cz>}NNU@w^Y*vV$95 z#4j>nyl<4Ao@8m`PNRynG~##3(%m z_&edsx<7M{Wa*xy@;^rDu}Bf)3C$W+WI))5oTU05!2bc9 WB;-)gV|Yyf0000g%yiLuAo;3n_NWdgi;NI8O zTd9#=If?bAvUyMC&u}#IWdteTtLCqt6-tw*3nnN>m!76Bu z1Iz2XlD|+(Z#B|ifqE%``ifgl^ISu=Nxb&!TTD1v5)f#&jis{b#&ozp_B z!|Y*f^{jhpf-UoRE&omLd$GLE9EFVey1!>w4DodGvqLUbpZoeYh_g^|J#La0IC^|5tmBpSM_nDVQ0; W?{seY2DY)+;T2Bd7|QSl)V>4Ss+Cay literal 0 HcmV?d00001 diff --git a/examples/icons/scatter-plot-16.png b/examples/icons/scatter-plot-16.png new file mode 100644 index 0000000000000000000000000000000000000000..3bd036a64fc23d2955715ffffe0172a004f59767 GIT binary patch literal 430 zcmV;f0a5;mP)D(=lt5K@bMuXOBa~B8rLxi&Tq(O9T91 zAR=h#FAz%`ORHF@Rlr6BwXjhTf*T<$B4QybhhRXk_}1d~=uwxQN9G!C(g!S6kj&+{XeQ?Nq@C&zpjR zQONOpzw)*UYAUddDtPbMjAP&-@LkH>7QpB-{ zQw9eI2d5$mA|j=f5Q2HsR*Q5fC4`_=^j^Pn-@EM#8q72J!vD@a=iKw3bI-l^MX3Y) zhK3aH5q1BdQiDpV9KfUsKf9q=z@0<-Xh)m*^rbC7a|&RLHD!|K zf_Ix9t+lHk|J7PvH}Ojk7nAZ6vqStEdzF${dUX>0HoT|tp_@n847y?5Ow8}W?}hxx zVK_bm%Y`h^lX2|3k?rxn@Dpq6N1g}L2U{<^6V#moMQoSs+B`jrK83HplU&pn)*td@ z;dm+i2s~W@a)s;ze;Pc4m$lI^&&-R1ahiVK!1n|>5zgL!W8VR?zfu298V>fr^z6?P z9+Tl5EQkEIe^14aeGL4F>~aoHwZu_Bwmts1j~WL-iCl7@u7Ppz5mey25b<9@m;DmR zJ{98}=(a!fB5t;0)_c$P%J$1_lBXZuEIBj8+4VA~7uzD(0<+j#?h!Fz{hs-q-_v)P zPp{>C+B@C{w+i!`FZfod>x2tL&w>#+ZEmrn3xD9|#3lAV<}M(gBQM+e^bCCqxSq_2 zof>l3%Ipz*dY&5WI*@X|{>UHC<;HN(4^y>kvS-QMjQFSFacgZb?}*rub!zLFKZ?nb zW1d=X5o^yG+_LxX?#OxQb^MGI@k^XM#2p86_KR{4$VU!6g^ceB_3QUxMCWhnmUF{* z*M6Itx_q5V;(FkCL9E=V z0J!~}#>V$W!2hH0=XGL!sY7~1e388#aCK30NzN8_IZJZar9Tzt?^eQfR`wIQN#4nb zf3;iQcs!hPj|W&^3RwaV@#=)-D8hB1CKj+Zg(Wn7K#@A~slCOBzJNJ1*wU zFwsPrK`%V+<#F#h|2hA?2dCkp({PgaG-6;7n0l9>PD2?efuBZgMDGfGgYjM99%fez zV2IvwZV|A052zn|Qnlm>fbDO93IfB?2te+t(h_hDa|`J=r(q?4q;VDKpb%p~T`8y< zeWeCgA7&6Z1AL48Kd=mQDsGTl*Pw_S)@Av$||$KcgNLB+W33mIWT5(79gr6e)&x8*}Su$4;2z!xxBub6zEM&#t z$WWF!4lGqx`2_iKUagTzQ{pnP4z%1biM?0inavMTEqS_7>*iYog#xfWCFu#U0yHZzfxX@ElSD~yUbW=w zw}H;LNYLypufSpb~JY z3xX#2!g-`Dx#~1*0(!B3?Sg%?4SyrbzyqN90&{Foi?lruUk%56 zfw}qp9wWwp^)x~X6?)ph=MCFPf+Aktv^G3{2?TpvtbWA9a{vGU07*qoM6N<$f_j5t ALI3~& literal 0 HcmV?d00001 diff --git a/examples/icons/scatter-plot-256.ico b/examples/icons/scatter-plot-256.ico new file mode 100644 index 0000000000000000000000000000000000000000..588a5cf450cf546406de7256e47e8598a8ee5635 GIT binary patch literal 270398 zcmeI552zJYyT(r@CL$&w5+Zt#2xe*}|5+4yE zA|VkGArcWG5{HNg36YNo5s5=LA|%AyjgXMMzi00~=j=0QX7-+$wbrcJ`#tfr_nvpw zTJQU;_gS-M%^xQwZsR}w-8L~fv2k+o#A?28>~5HtSk&FFpJo28{MW7J;7RbB!8ouS zXmt+k9#;GG^HOja`~hwmj04Mok;s8l^yBiPaRB=|aMAjEB>LONZaaRZbKnBnp3E8p z*uECbnD$NkmIEW51DEOFmHlG?zZZkAtlvkv-_yqe`#rw%gO5`Bf7!He+P54Ss~qU8 z3DW19_uE?E_#O*v9x$GL|ApoNX#Zgw_s447H@%Oi-qZbl6Wwnx{hRh}{2$vKc*^uY zwtb&I9@y{kn-}OlADjP=U%#8yt*20UHCxkApwy_tmC< z%YD=TSmwaHcKvTG``wQ9<9Do2_qmS!8%+PEe>?V%T@L(BpFbn*>yPiY{%^;F^zp%d zk7iEjo}Xh~<#+#Y`X9UT-?TlN+8)%en)AN_7MuP}^W?#xd~s>lamOb#r@i42e^FN* zNNZ)F=Wnubo$YB{V_O=(vrV`Pu7FG6dvFeX0^R}J`gNatXS>V!zxqMX5r0|%A5!K- zKpdy7W8m$8c$2mc2gZxE_k4qRj<%lyPkO|D>fYxFd#Pt<4Ln5IttIdP``!mOWOKB>p0{x#-pt>TB3F$Ro*@C=y#!*gye8UwK~VET`xC(I9HAUp%6 z|L~k!i^f1K44D37=?U}07zodR=|4Q@)}k>G3j?PASbD_Ka7F!44D4Ib8amf1F4k2+x4& zKRoBwqA?H)1E&92dcyoL2EsF7`VY^!wP*~)!hq>NmYy&_jDheBnEu0aZY>%Eu`poz zkEJKf4`U!a1E&A*oDb6?{jwP7U2l3XoZfBs5;z3(4%gR#-mAA8$bQ!_b=p40K$-z^ z$KL;!CZ)mO^oKOBcTj!+E(5*Sw&-2FvR?rI2D`xW2Hm%&TgCwSV)~Du&*+D{fR5=u zf}5a7^LaMCfB9?h1XvM4Smr|H43H^%LAL2d{zaAWOq_w_T;)gJ9mj`O;9h)@+0fkQ=7|2KoiwSAtVObN0GAFDkF~ zfx}=)17TW|kuyMEnEnIz5u8i=9|D*2{h~2I?dY0;%>x4Wb?nWN6Q=)wodn-YfY$0m zX+IekG|xB~FjQ-*1q_fArvHHb1mF61@csgcE<=9)LA#m%4rD+Itf`0?ARk=nU-`8u zqVS?}1#a#IGa#hq(`|i2`%9xD#++%40dm2${x>!jOvDYt^CIvrNb9kQ-*z4lajt}P z&Ga9JZFt@cbUm&Ky{Fr}j2|`!2-EdZw?ZD+@jpNh!Sh>@Xg?VX9uE+NWkxoBm_(8TmO9`X>$@)AxhZpv>|1Z}!)?@IKfEoIWQ{ zeKF7fza4u4j5z`1XRGy(|4V^%ujg8BfI9Qz>y$qQ9so|}1Jo1q`rjvGp8jJWKKa>N z{p0&2I060yb#$LD`z!Sw0#4qcNj+aW_Al*k)!#?QUI1fG0D0L;{p0r*aNeV?bYAcY zSX(zmsAI~bpH;Q5a(nB>X64(>fO5cLZU(vk2YLtvi=@o={N*=rHh zWq_P)jsEd<9niJALAmYPu6e*mpz^=1L)q7W>T&H`)%y0-%^fS>eg??LR_Gr;wa$OM zLH%gH{357yU4U}B58xM2Rj)3~UBC~g*E!p-gz=1kJZOvlb&ap-b7<*zbJa|t>?=U) ziY~QWwl1}A$Ns9FIAZ0$KI*<)ms+S>*G2qURW*Ys zy9nt1!cglOzth(4s{UHJ(PeK2RqsD8B?e3$6m!{`J?VcMt5U zAb1(^@_UjS^4-GAU|T( z|8o43He%6yzaO>M0Qxh7_;h{a1#rbttUpoDOTfwVOZ~pu_t9d29C5AxTKCHLvx{%5 zLFjvZ`!!zl{bawdF1{1%BsdJTzEE~Pp#6UYdZ)>x3xBMR(Pw~MajAc;(^bAtKFv0D ztpPfJEz)6AHgE6FBwmesy5H#}I1jFYbew;)P4^?o{yumV+@0poej5Wt43IOf_3!NZ zGQO<^x^9)u{TP0qDdN?Yjo4QLT?5$)c7r`&CwKs8zTo^BKI&WqGzQ%d)Yh&Zv{rE+ zaNedXzO0^d4A7Uc>p#}x{|Dumj(czNU>Vp3j)HH%WuWVZe}Nl4=)1<_AHkR45ZDCl ze!{rBH`hD*E@u6&!oS#_n@#2(&es2OJ12kvezo@aBKRKY{4I%Z*iVfIpMia#(tXhZ z!n7uG7@*H$*8g(+n+C(?V#xho%%QXuo8eT)`vc%VpzK;>7SDfD&nIA0D+n+rI}FfY z(du6g;-A*GvhuCTZPH6?pP|F|)!;PHyxujBT9ed#!sY@k7-Q(C*!BOkha9b&^IDUb zY(e)fa1p`{`KArtwYt-tjqFW;7{w0Z}_Zh1~Y-U z)Ewz!U}HdBlAq@d{S&+Xm*V3Y;8MSJ>w0M(i7og6f0ICK`gLP3Q(j}hDbTvHvISl0 z#6|ihYW*une0&VJSfeVFyLtQS`04B(M<<;)pc4Gm82UBHi^=D9oeLZq;G&ruVt~GB zgZ@|Ir>;r+%+q1(egi*Rb-f#ZUja>ypE`egu%Q&UW(ydgU!vE)+{aJd59?#SdRX0S zzcwEGwSIC9l!?>FzGv|vs`CK+(Y$CA(EL)@EHt+N8@ve)fdgPG7}@(BiLEXAS6}IV zcpterOr3wx_Ce6LWB>66IqT>wb!pz(@EgCs4a89y zP7jCVj`;rtE(0IqpvE(;8MJmTfbXr+zj6nkH3mEcegtK5ugtz0<4=If@A(Xw=jpca zdo}nIl!-aSzSC)6ef%bF&G(-MS3Pr}JUI>8nkTK(f08e7eh>Hrs1ID`uGV{=1yMSw6TyU&;+iIVDQooK5>OWWW{XubSZgB>*ZVbTZ*6Lq5g#XLH6W~9f?i^3^ zxp%<^(Dw7X7l>g{-Zo+Pz6LlBOD}@f96RuJB=n!?hIlj| z(0PH*{j}zL4x9%UdXUyngQMUnum!Ys{U(V!{Ygxk#|+Y96Lx82f7)Lczo}P!|8c`{ zU#0ymF5(GM2fmJ!{`)y2Hk}75Csw2o)qQCFKD+u(yyK|kE-+ zA+1BL%{YG+h&mqvF&z7oct66Q=+~0)(ez(ZTW#2#e%%RluASs#ET6yOlkDYGasZUHflbtf_%j%Xvinf@c{50|dcw}*f$ z-MXkp8r|hGM%6`~x{m9@Ki75WUX-Zs2f{y>`oBhfce(CgtFIaZ^u;<*=3Xe*`Jb-u zJpMSp_DY?xUhB|(NY2+fs%1lVDg1M(f4xg#5o$&t-~gMYc0~t`7rf< z5J`+_UbWWg?BB!nk#YEE`u9O>Bz4li8$k?nfba0b%UqaxPlL4fV)(7&;bbJKkm&~g zxz_*k%!X`ZhXMH3TJ>(A`^0t4>Ibl8iQ=0gbws+T!Bdo!1Fes=fkc7-2+#YH;GMiX64W{H~=c$Q%X7A z8>i!MQg6sljXQ6HMH#d>*+ktRHx%0-9@W=TzdHv1O#e=@FuM}?-2ilcT6YevvEU-u z1xjm)GI}QJH!vt?UE4Lk@p??4e$As?`|YB>|KL~D?*za<(|@*fYi)(s#o&1$&AFU= z>l&Jlr=`!cD6~CiW9|1~=V4%dO7 zK#1JRYvWJaf1qY46V$O3Yygjd1K_zH4)(U)$EURB{62mMzH1I39`ZN}wOz;lpyxnU zxx-J_`fqFwSe1i1<={_qX!Uof+$w5k8b6}bWgULYlvV8DM8kb?h4v@Q_}%2b_-gvE z(E;$c4O|CBx(sF0+QZfw;k3Re@vi|F8tMN(l5B{1y-lWHtR;)Yu z)Chg@AP9Z0qsCsX!L(xDpYV^rrvHSDlAmyQH_&`9t-&ULYwc<5-@yO}yTR3feRGVq zqQ2KJE4}gA^q)n{5ZmDHU*HC4Qtw%9KR$%}Bfks%vK{>5xqsCEM?vG)K&o^oew+TQ z5Jwrf)0%je{+izQUd50`t{DBJ>zZdh^^?vq_JioJ^XGLezMKB@$QiI5?hb>dwVu`f zxA@hn^UwiQ*D}M!Qt&cxbv~eVzEhy|eV4TY@lco;O#c;mg*&bHWa+QzZ8|Qs?p(BD zHw;}2PF8~{AbzU!k)GlH5Zpg>qml1OET;d8eFS$}^Qn?YA`9E#ySh@|I1Ugr)!h`1HM}hbE2373# zktznyrvHjP3U{x9Dmm`E%mw@kdK{%a&6U4&$cN6m4}r0nPgYFBTE*bk^k0!dxYPXB zx0b8auQlpm=hd{e4`|L&#n(Y)bZ*d!XK5>@QKQA+*0uh}=Kg=U+YMs8|L?gFNk#0M z?`zFW*TVW}_iS|K=h-3Gk6*8kYd*Whj)m;rTi+jUv3vu$*pC+YfVL)zCm zd8qO4I&Il~&FPM)`5R7M>i=p@cUo5*{yqY(HC(5@Y5ehi&5Aa(R`2_`pXNbp0^3@b zBoR9dpQisB{nOJy)wS+)e!KX6phh@D6(#0L@H24XZ`nG2RQp3YYC8>N04`1c16j)7 z0sb@(J_X9=v5)<=?(g-wAN4N*F4y_1@Nh zWy5j%WKpQWCqTCK?`x{@puXW^gKW#4s zj{$LCwa*p9=U_W%WE`Mf(|=wkm)s7QtH4(vqz-rE(+R;=^AI6tJfTMTV_?_8hi|VYd?ui$Bh?(_c4GrO#hj^?q(zW zYTkSfXk1VFx5}q-LTgQXL9qFuSfIVdKdmOT?;X59M^d3JC1by%9p*_ zUb)*!8>WAEbY|8B$I{Pkpfw&nCpYYQf%enAg zZx`E%`B|VjR}-=7o|1cA#F1AA^_%|l$nmfpzSn^L;3)VM=zjelz~$bj)|GXveG5qQ z8$rS#bKhJQv_9kN831WG!OLIam&(%|}9sg@*%V<|X zj_CLw>-ygb*X;M=J>q#LaoMM; zi#SdHu1Ouy`p6Zni`Bi4xj&!le7}M(Yh6=Ko$J6)ppI|8%6^DnPVWVyKGVPJBpT8B zlzZSf@Ri@g)~jQ`^L2l98})7gGZDm?#f7fHxIW)8{g0?#eS}5+O@f~r*2nj>zsgg5 zG3);Xaoh3VN5@;8Lo+}QJ_ee4CgvJ#`@A1^E%oc%BJ23u^tQK$?xZ1iBu+d256PA7 z)@6X4(Rsj|K=bgBn$VbY7&tu!_V+7wuL3`UkZ}#$)~o%#y7*3dV3a5T}b^)zEGB#mfM>vkV*qzRv~z zqzxVSovwZ5F-tpoF58!etJW(H*5vtIbvt#s)c@tGeptD7GC=<5*`ODIulwb%_UiXG z=T9?Fd(VTaI`viVYkYD(_n}VHe>wr|cWDO5qX)q>a5WcDJ<9p*rJbo+fHv;~y0_Su zmaEkJdd;vhixZ>iKa&~TSe5~DNoxWJz;{6N)hcw}B^X;s?;>fej#&oCDUAcWf$kN$1a!^rCdkr;wkyA{0NvlV7c`>#*#P*0 z51WCWxy$l7^tSi$!^_$S^*-Y;MmzuSPa^v+9W4i2!G3TE90l)zx4^6539t<~f3~$Q z{rEHm^2V)DV}PEy+FUo5yz%Z$t{igp=$z<)cjsf(cwLob^>yj=sJ!~C6 z)82i)3G2l+tWSgXv-!X84zv1$Gk`{PPoU=Ee}h39aABABcZCR)cI@~cf@o`_)eN8$ ztz#YoRj+FfJ1*#Y$%CQDqAk;ZD6*}cRx^NBbg$!6K*#4{^*ro;np=Dgg8h5u>I~X5 z{kJ+R&H2y_pc!4y)Y_b@HGvuGdKq|K>&xPaHckIo#Mm}tpnw5%qw|4%;7d^Dy{Njj zr~8YJ126aX6}UfZqHWXvEF1R47%0a8`qA3pF7Ph60CaAk`x!MR2seP%41NPw!1v$- zp!0|gAlP$FS&`Ac=|77Y+ZH7QI?G%Cm@=@#RuY%t} zS)C8sU*`<31DgxKAh*)(kL)M{%NgG@A}jAyF{CzJ~iGP14~AM%PKJtyXn6QaaP6&1LU9XmC<$k zP?}G-qickZI|-(Rl@Pn>zlHra3NDj-y01rb@U$jF|Gt0^V|gAx+@}9g5K{|dAop}{ zY!mbT^!TFlfEQXwVQrU9|FtQ!3Mw!_-f7M6G)QZ)so%P9cM=mx z>3JPj&(FIUGc*s`Q!lFal{Wpiug^xoWAg4L;6k@9>(KoNjXsYXx^GSYp&7UK$T`jZ zzW^?^>!L251B}(Z!NhCxf0JZT2FSU)!ByZwvo7niqY|XZr7F!@h@LfSh|W zV*S4uf`eGvF#X5U6XW9tIj8sWxIS*Xs_*3(dIEm*u0Y)zEw0`Ir-1I2)U(SUf}`LG zpnWR;d+C(5^Zyt+e2jb`=k@|udUajT(@k{+oanm9cJL1P0sH|5#dn>3E`Ybe7EtQ_ zM(l6;Z>qz`v|Vy;8yJ+=P1%Fr10>A?t%>NG=rEek=9-wlr1_84~t$hk#8_u9J7 z`MUL;!KYH^&sF-e7yn~?{u+RTE3}b&K7h@o{?Aq6!^(^w1LU3Zzph?gme=v}sSrIu zJC6g+(OvpqRM!k`ZOMv_?Jo6yHmeQWW(=emAn#UzOQ0xkL)xT~4QW4J|E7%x!OsoE zeVI1brQ>0n=|4@1{r1iPd8c!LDG*ZQMQ#0CKKTrs`ZeaAZ!qRBX?tmZjQlqJ_Y-2@ zgE2tvtpz$iEz)HOo6f^G`wpBov?j0fj1V#9wK4BKNBCm;_ucVUe=Y;$-?I&CLVA=g zeINUEZ_ZVar^`^=<-^*fL-=m`Psp&(;0%y|%Yp7$36=lpc65zwmG8i5<9LHHrel69 zi5H(t{|OoP*$4yV;5u-j0exuAFZbG*D>7)~KJZ%u@uhX2{MH;`rQ+k$^zW*pt)9vZ zkcamJ-G7thsP9jm^Y5v;Jsb#DdUwcpd1&TW1HP3t|~ z{&jp>3{3webJTYC%K$mK61)Ojo-<#gzIVXv+PB{*y0z2ZF7PKv>o11illW!&50MDg zMkxl!OU;`%fs;VTs;YWX*^hya`QGpCPX|ex2SHWeeV5yfZ>Ilrr`hjl7$85F0F47v z;CpZb4AO=6xd>hdo58$$-a-aP+lRp*f19#DiEpO=kO^UJRc3%(y$eXskM{ERH9n8^ zpzq4%{XqK+eD=MvBP9yr$5GIfz6Z7ITK}JwpvHDJ21cF%wEt$r`aghg8Z!rt6T{j& z*Azy+Q!PMaU`Phg)~gQnHLMPY-A`+irvD-P(snZj$})hyo(8_IWe%(Jy#4Mq{g>@g z+us-%o&ogrFo^N`pY8#6ef{6&|HG$-?KuVvps{t}7m#-hZfg6-q94C({yzqJXdyPk z0Q%B>44+4+|EI->>A#u0uvW*10W|guh-nQ#?_TfR2Wk2rpFFfkn_)m&12dqhxqiCc z&fH&YnEso|32Sw{7(i!lfEeZgI`+@L>&)~&UU_KIHp2iqn*>)t`dA$L_ecEu2dK>_)2y-0 zrT$N(>$l&=fH5$~0D4;qz5t=;{qNG|g6E&vX8ND&9{XktcxM33tp(E{)EJ=mD=*Iq zjP0iXEMjb%G2oE_bhiq82(%vWYaGy={}@=77Ya7he;zru-5Bu5fOH8CgF5dw${RaW zr?kDKD1_cVd3BWCZs-3+WSh+xNHKu^bbs&z;4IMbx~%5X`)fY&5x5Twe8(yKxIF&9 zn+nf<8Ux0_5DcJ0%?0)WJu5KGJI>O&|ATU8z)nzj%DR?f$y$yD?x4lw&~J z1)IUkKzjZeWUUcs+ZFH`I0)8)p>>~lMH$n7LWX@B0}(MGJ%bhCZlEz?OAngcuLiP} z)_i}6lsEnN6Jpkq5c_Eij1dE-|1s)p3)L7%G2mGL&!?)lpT>YO5HSO$|A;%sTr&ndGvHAFx_9|0 z&+c0F#(**4kO9Z~Kj6?ctI8NK20Sx>{+|WuX9=qO)-wzHJiBYv8w19GLk7_78c^kV zqx2Y|_ZmLz&^4>d7%&C`Fo14PfwZ2>{?_w{n*z9NO&9~lfI|k*t)4B)dd9HK7@+4A zbKf)M&|Ry_7%&DLGk|uFgED&W-&bSDLB}pxb;f`(5QqWvyA*s=VGPi9Kb=1;c(-sM z*R2_2z!-4A02(0kp06lfMmq1lPbX;A`+E*bWB1TQste%pGIE7%&Em0b{@z=rM3>z~9Yc zrq*9Cw%<8ZY`H-2W5 zA>&J_{P@3x74({czuEW#t`-e z$<$ZBhVlgg_St8Ku-{(b-!$b1*%uJl*S^XZ*{6oEFX({2`c>YO{U6m^Q2%vxY=M1R z9aCWM5SwpTW_8PF^QX)@a%-CZXWNxoI|}T|ob|V6_6dg6gj)4&1}OZ**_8wH!kD#ptDV7q zq<@EweVVOZyS@#w?^sZNhEmT8}J6k(Fsc*A~ijKXT|0=|1=blq z_)P3-FbPC2-#=8R>Q})$yGWLOM^5<}EYk&cH8q3%NKScC-qV+=NykUu z-KLHg?^$-`gvLnC{qRRlY9F zpDDI=bGmCMjrp77zu&G{`tx6VvPr4m`BNSj*_DUuJA2O6-(NnhPwgsuHzs~e_2}>O zZ=QXoNAH}yllrse=Z`O{e!hM7P%-P@e7j0?{xa;n$nxwr=AvMi+48gT&AL9n{#pBU zGPGsb#Xr>S=$LZs@U*_bt~ir4V7EE{s8=G=QJ9*5|(*V}q*ECqEVDTzLwp-o(zQ!kn|y z0rF^?;wsOle)ieBUpw|AGIXlzv(I$;rO&Qv`s_EmVNdW!`QAsLoxS<%mG7kEk$L4i z=6UuV`F3@q{K>Ar+sz`dw0%Yuce^>W{4jQiP;aS&7PQ~Xk3u^glWUiu$llBE-14&H zSI3^kPftI&c6BVg=i8?{-QVe-tom=wbkZ;1-W^{$cG_9M9|N88zLP(h_FMmpxoba? zRbE#92%~$XRBai5Q`p(Ou>Ib?(+l)5zY^>zea*MeEYRnCI|R$K-1Z@8N0FTXj}+Rc zpbzQ~Tb(>JOjtNsD@PHL}LVg5484`olXI-`6?TCK4EcV_&v{@);tisG9g zjf(73q)CyTek!u7AC6>{Px?oG?kKQp2wh)b*O7wt^`xh|`Jn%^UHLVWXP=t0v%bHu zyzEE1M`ErIEVNH!rx|@@+Go1@%C-0OIYoJ{zbveuz@_h8I|Qmwt{nn)6xydd^3E@R zOT{}hX8I>1f4HCRf7e{#Uw4Os_B%VcuxIv9&%bH>uWQfDpI-el)L&%J&EH;m*|YM$ zXYNcfitKs%O3LTyGqKZwnfjl#Pji^b%HL#%>!fLs{U20TWLG`@?dubFGe4*14#j5A mU|@OxKhv{E`i#PCni*ifK0A}hF5f+=%C>3iKN?AE{QW;pwgE)| literal 0 HcmV?d00001 diff --git a/examples/icons/scatter-plot-256.png b/examples/icons/scatter-plot-256.png new file mode 100644 index 0000000000000000000000000000000000000000..8c3118ae87e716e966e5c5b0a4fc376a332604b7 GIT binary patch literal 7831 zcmb7p_g_;_(C$e>5|9!EDbl+Ff)aY~RV9cZQl*K~dy^JKK>>jvEz-XTNRcWcyahwAHfZotR#}oh{;8zGh zjQ}4ues5gB2aS(`r9S{Lbp884(#07#0f5KQP)E}&BxjWzmUwSC@XzM@f$Nf$pwdWS zgv7nCTMd=Bm;7v=m>69~WtuYa7#f5=*)ZaVTq>Nm)bgj<(xtxjyIE$x#g&D}F!afu zxUm+uHg)ljz~-v!whV!K)*1ILB^$0hm|a$WrOeUyKC1Gh^W~;WWODn!)I#u2{X@}B z`}}&|!sa`pkY%X>O6D~ar&yw)`;`z{2&$C&E$;-BT;R!7)r zqpg5Z-Oe=AQ`s;ec#zzQx`w(tH7A>9D(nVoN@p`qv5FwIxfP_v-+(nUX3hMu+UBy+ z7d1~sc=1H0@DO~Hs*2OZG@W**zh`(YoCoLmT?9BDqBP2ke915qBlBm=K*38a_}m53 z%BAQ5qv5P&U_(5qx-@Nz7zhlS1I1!GasR|3YnNHdz@1>v4YSB{q;S7NB-Dn5(XS4@!9|AiW1| zb*Y$q3u+5@oIbnj`#^gzXa zwd?9bz@(h3BKm$IC&b1L1%=80m*;YG|9*I-kk_O}shYp)_%Ow^eNDyQ@Q>aNpIUk& z4j^r-x8%@H$7aBaH%M5ZL{D@$A%pMzX{{pLzsVDRzbi8$f^c{>a32Q^ISRqOl0b$# zVDhC}w`*d^t{jgwI{52e$ayVB*NRccTn~poAilP8I}7I7Sl(4XgR(<+VKIabAq?v2 z!v&$*juQxn6|;$MAL}~>o_#vgLpS02JsJIITU{49$N($`bk7jaKRc(Gf}=(q7&boD zR$U#^%-x)|8}CAb7{ANUj_%WS;Q@k#LY3xQ-qtIaY@Lkz^3AV3FBffna5{j^2S&cz ze$IhV(3BnmtdFjlX;tq_NYBGe1i+FmAl(DQ0DJrm8@Bkk9Yo$$w151nLz)94KKogp zgMB_Y=7oUtCCF7lr-*y%*`)$=&61ZzsaLcgYk1gUwgPT|8a5$O;;ul&? za{B6b24vN$1Or#KC$C>97Xc3h-jR^=>~@AOp(%lsWBRFGPEbGe^KZTl?LnXbT0iW@ z%SVdE7aP=);A-Bfvx`Rrk^H@fL(?LmN>m(xS%`(;*YLthsF%2d8fBvE8?6Gh$HP-C z81`JZWG`8BG`|zE0T=p6%of5l!BFQTB`H%vm_6C3LWl<3VY;~jNA@d}fZoX*q}>;( z>D*Rzse{vK@qPyiwD8Gk#H7zpLnO!Xvx}M(5ogyL55!$nMd5MqvSI(T3Y1Ho;Y|=u z^RdFDmQJKNVDOyjyHE?E(3aq>D4!F8ap-B5Df3v>?j@}zr&NT`H?y7xp&ML2unkR` zy>a^R?tV=!?Jz*>34|h8$SL)724|#bCmgDjH@oOhFeexv-=Ir^c|-G!wsvwCBVVf3 zh&_RK4WD>hCElRU5a=C}PstmcA)JrPRHF*#Ztj}ryKx=dwFAGemo9I??BPj}p_iKvj1teN%lAUpU4lQ(F?3~&a=@$E@S&Uv@MbCnWUpC{r`_r` zbbl0V?E0pcvMfplc*E$(0BmcMMA$5UC;sxk8T9pv{90j?R zG(Aw|UyQzL548<`+IWH&lAjK8+oc5Fm>d#z$qxKoTwhPFe(Ke8VM@3knl}YX3OyiN z+&`t#Z&5<6JFDeD4Nd4$4_FgyL+tz#aNx_Ia3jKLL$_n)7*yOwrrdSGC0xbP7pOfi zZQS^y_XLD3_{-VFYd%*U1_n40KZiG0QU4riBD&$p#dC>*vRiiNeAX{FGXN(bwm!A;^T&v2soA@g!-jc3#g@Irz(r&{7@N8A%p0}h>(@-hmbVW#Xz2klF~d9_O`h)< zL0t*k9YiSGzcUSP5W49y;LLzV`gTRe#*^BR#}`C_mP@k?Iw1B177$6k28(N)SrL(r z45>mRR}>@BKC)<=&;}yuPzZxml?%3NIagqo4MHHUfsjC4?DprjeI)9mz7ZoJEzWIQFAO5GTlDHywZ0qszJgjM zN&yy9OC*)?RH^?9C4HZacizIiy3S&oW}+8Bc$qm+!-65u#mu<`5`M86za`R_2~10% z{U5J$f+5MxC5mb?rgby2A_L%zz|gsYA`V>_R!Fd5oL{UDtIq@6MQ_z)H36e?llC6t zyj(0P;3_f_W|*c3J%$=ikYir2bZ1mV!TglGFVj;n*meFu+T3Nqr1Hx0xY4xH`V{{) z3x1kw%=I%OZal#eHAIpe#gfuzoZK2zOe8%gazy;G+wvv65|DNef6eGFQsCFEwx0== z=lAZv<3i#z@L|_@M9l_^(RJy7t1t^Njz0fjouZS=Zo{AgC=9UDag)uGYurcF5j?s< zD}gOGa%^J_@PshkUq`b3g2^@%z&fH;uCMt+K)GC#1cAY`DRzm67oNSj-kum|V(zY35hh z%3(COPq-?J=#kv`Gv}UzDg+rd37=e;kx;<(9a=AK^KdUAb4sa~IjtM|f_l~wRmE-3 zXWtXv5-e0uNW>qH;CmPEGRi}T;+uSpOVco9d z@b`(SW^dx0dC(W`QWg*Wp9P18=PgfxIn6Nos56)?vaz#qMo37z%B$ph$x;?5PGZKH z-YJyDy2q3Dsdb#v!t@-J^?WDg=z((K-PI`0x%K$$el$f3HNq`CwX+{nMf(gq^0!ur zuNS-jgV@aS%iFVyUgasuZ~b5O%FVl)R2rEKxbJOKGcG^>xZd)A#L2NE!tZiw zZC6HY?1peB1j!M$VW7a}e$QF$N@DgKl;wFJPufW1OKaVnpc@;5&i3{p1k`6 zdq-T(IP=S%@E#Ph1iNm3M^ zdzZL+4wJ|0!5FI7pL%5Lj0lT9A!+{1M+wdh>_taGf`Ax)R7z!$5C+VlDz*F1Xe~J@ z=CfJ;S7m@SLX~+w%qH$aHoN`q`U%`%hZoWaMgsN_(cU2w_a6=7JlV60VY)7B5IIJd z_0C8NL;l&k64eHnow*v6Rc7LB1GZgNH+K1**6Xo7*eEfyzeDe^?<>c?r0sT>Gj58v z>a>%XAc4LEL;n+?7=q9NdhO%1FMhr$&i=j2p%icMRLTj`>At_MS>3-x*s1gm-J%#% z6u(ReUUOMLDF1gQHwa#)p{%0!=MGJtD;m6C;$_48=ECKLmCqCDp#joro)~Iqo zPs|>(DBFr4un+|As~i_V2%^@nf78tVx-t{iN*D6}bsm91 z{?9qvzaT`jVTWF-L==-)cJ-NO#*Nc8R??$$lS5Rk75mQAsod|nh?09Z& z1%NwyYcNL%F~)->7|}QcJa?C%)MThaf|GGA^ty{Bd}*y;_|NXcPJ0 ztiuF%2jHcUg6qsnvrnfaMl>ElydN2}KB5_uSg1#@do%*s(5Wb+-9jFC<2Ys~fO@dK z9;<$iOuZ~5G)Ck+(moY-?pAtYOOU8-)2uLtY3$u$9x;AD+>ty8Rz)Zu`nk-0C2E@o z>(yL7k#YsO^wnUEa2@wTRZ0=IcfYP=OQcTH?q~M?HJ_Kc(H2Sje;_}~EPSDxxdIa! zrpfJfU%iuI`8`n>3XD>3n5PDUc*D$$!`q?8;o`2)#2<_IJ=p8qe`ENUe|7AhTcPT| zx%FdWNiTPJ(Q`GXdZw%;5Gq`?AryLeu8y_i^RN3T=sn%KnINUMs#h8?jo?Vx09Z+F zTI!kSYUA*B^bl^fKKD+|7%CjLpDTp<@T-C;iRl$`FgJRTKC4>=N)@bpwT5NX%6UG> z*>LLy<$|-LU%4DT3}ILj$`N9nx&LKG5O`QE&_+2!1ktW)@5YcTYAzh$IW^|vccEe0 z8Ortk!U-u3Ls~*141(Gzn)bnOWXNc+&`bYRvWmcv;g``v$!QOd=;|ENw^bR`#Zp2s~4`|g?egG`{Jsg_tBjz8gikw)Lf$IdYZfWvt`>(En3u5 z=9SH7Y%P$X(H9=F<{CKs`$iROhGUFX)cz%9hGwR+zI`1Wey(bqJsyEw(+PX#d^Op6 z!Xcsf7`U6~dxnS!tu!`CG3! zsVaHuftuC7^lANFBK>6INsbW+20R-Ar%kQ{kW;$mYT}Ki^!)3fhtV45MPhqyTPy8w z;Yf{frq1(bvs_Qn(z4!&wXJsMAz3o*V7vaNg>a+o4527cO^?LWzAGP~_HCpLvBBcp zP=J1i9BlX0xYGJ#N^j}=aH>Ra;8jz0;V172hXT@Hq2v#quV_Y#LBLBOO~$1WtypVB zI|-iTl5gX_D~|SX)1fN-no2Awc%}A?^QLVp%^gT4(yP;t(ho-5@*1YwWt&EosVZdm z7(x?ulWMbNdZTC1WDA|>#-<@<-}02me(kQa3$5w19ndvmczCRwU4XJa_D+xE`@{jQ zomO`Dzm2;qzOOxko^9^65?X?KPIX8hOZ6F%qd-2 zNF(D!%S5vWNxi0&uE7^sFMd)5T(cV^K;HL-(<$sc+9R z|9>xY5aA>J^7}gzdDYWppSuEGx};=)kc`>IH6ctjq#vHACD!*&Nrqh;+GA~Su$n9q zdiV%gf6~M6v|U#}qp>BpwyPq@)Yx2yJwq6BY!LxT-G>xJr|qx-)qJ9YRRMIP6M z>s2QlVfV06u*N3kUiqihP~OEYj$AiBxPo2h^vB}bp!x--(5e?M&J`twr)Q^UtXUWa?Q+mBx)q!7qu)MP@B}V^vCaa$dUY89l4d}U4QTohpU^r#sD{T zrv>xWiy-8W8)h7tDmpIuVqL4eBT_adlj8Zhyl3=En~C(9^yaO6Z~2<9oq?@?lR+jS z8{x~^U`Dn!%-NPouzi3WPfvdArOostV7 z1;Hmx|KN=4*mbYW^;@y6Csy;vRtIbURPY9EI0UrvCMKL}S zc4OY8D!TaB2B)^MSyG4pQMTm>a&!hQCO4%8l3Ld8fQ@*3kZ3B|H=E(WyDR zLdc3099qHyCeAL9x=m+U^bvXKaF4yFh)DWM5XY8jjl6}|=FwkEDy@B#TG|Pw8+xZF z+Bp;Qh)LuMwa<$Of^Q9FS$g01TOBMe zIRuGx@7WCV*0hZ3)472joVF!T`S8JcnVUxV|`RM^?dl!LIs(qS`&*gc0F722c z*EfO&u{oG(AlP(jb9Kf*f@tlCImTC}LI`*2Rbh&M|C&ys?q@(aqhAC)yrrxBT)M|8 zQzCo*MFdP8I{7%UH-bZZp1bP$){njRoC?q~w%hX`lf_l0%2E4J({Cc(kNldK#f>Yc+BGf1RP_SvBs_b_siiHj!lo6Ult6Tam;rJLMo-c^1U~G=Qp>CUU zKPdr83m{*k-_u7{h#l4_`0>7Hxr~(GsOVg#q z=^E67x}nQ0jw%~xN?w4S(S~NBq)wa{6~4QV#c%;(a3An^qHO1yPIb(`!1jfF?h}p% zbjz39q7y|c997Y`tXyO%Wo`6r9L@Bjz&rX;mwRHOTc(zD#}>}Qf2cQTUk?)bQZKW* z+v!?}od?NT63k2R_#5VW^Z*;)(mK8BUTP^=)%@hA5i5LjH7=#r&#Zz;&U#(-sI0g= z;DLJ5jnmH*w@=Raz!}X?b0x_F$V@NVfGF9RKQQcSiLI`Rr|mf#_LlRjHQp@)w@wF( z@!Wg%y?f>o@pFZ*K*UP~5pUKhFW&l*(1x=#iLTC!>7{0v0Skq$R@-@sH9HI~U*trB8pNZnG#y1S#!Is(-=?M4onb)^C z^~hUDAgm;JAYj^9Hf65JsLu711!RO@nj(2eCKe}o_8v@I-jGk-o+$x*?(4N)-On3l z?_39oXXC9ML&_e`i4;6&Nq+<*8aG#9D^gY7uBKYCe9l8&%M7`0ZSLz$kn+$agTSmp zl|vQIfe*HPMrz;j-_iX>N>qMwL*$goa4qzMSm^ns%+EXkU-;HQfKhK{y`f1$iiQI3 zfjp<))))$+Em`+tMQwMUrknotLc{1({Y9WVMgH{Et}n47dlZA@xzw+^N~s-SaRCfX@Uu$wIL8 zr%Q#;kGQr%HbL2qDkLI-)b=Fp_0&vDMEM?=(>1rvU)y3;n))Sc>$@9t1ue}Ry5U{8 zEi;3tbKH`pd=Zf5jMfhjHSQDIHOD&4PByWhSU0iN6!-N*J5b@w!C`b6{H?&fD`@}! zirmY2E(8KKPT%tXQA4ku@+-OAe*WQ^*{UK;UB4k8cKn8zm<&@iM}Y>v>&-ZQaXBUc zcia2Hn78iKVaGOC1~9;iR~*=mFoT=(11s<;8uBwkxnN7GEhBr0Ic^78-jGV-jl`a( z71;k!Kv2Ztt>0z7>Q4eDlpVj~t4rHEuiMc>;&p_f*uUD8 zUhE#P?qd`EY}@(TjkulezpgTnDT zOSh>3#vmw8pAVdV`fS`HAbrv`a^fA|U|TL!nEATJ#R6$}8NIDkcQ7YLk+D)&3#T)b zkK_)a8(nKIm;*Kg&$7R$-M7>&*@6`GX1sY+NF<5O_64CgkOsRGUE@Jc#9zt0eEQ~R zf~jLo)35eTg%|O_p!4)p>&gO1i$!on(8{OaPaG&UtaWCABmNM3bQsr8 zA@-)i4fP4Njn7K1YmvvxMrUxw< z>w0|s58_SDtNPzxs`JlJs}QK`cFNpt!~t`oM&X^tMeF z)}*QXsmE0Yq#wtmL5$Gphf)9CRefwG_r2)@G=Q4%lQcn)nD+$z0@8FTw54J8sQekY z44z#1j+KV$K0Ml&vTU$apDnM zF7mMy#q;%*ab3f_j^&c{a*M@R5kej<8}~p40a7LqPGdUZBPyMFffGKZ&vU#En^00E z=nN+#utcHIN5Cjpnwr1P_!SN&^%=ZX#G{(}b zSgyawg*BBe0oy+uAPZvl0%T6gyTN9Qxf96!{og(qNa?wO-G=|aVg9yE9t<^A#2c6X zF6v$0BenU?R<@y&|8T%hy`S&F4wPfB(5f-e&FmeiB({EV+FtnYcG2!oxzy}JWcm7l zSyc7>`q{>aVID)%@FyCO5LTmH=@~H_UQi|`DR<~MjkxU%VD^qm4-cj8iKC^}etqE9 zd)#&2o?3xw1(}z-WMd$4uKWun>`j^WSP|=u?Z3+)yi-rAZWgegbc>p!P6%hfPt5Uu z;NF&sAd`2U=MSIXGcg+0=dA(nf*(i}nQ{MxHO2h92(MnmQ(HY<}Od{e|Cqvc~{bw$lf9)NH{ zCkKp@HS=M+u^#EBUzA~!gU6raUSfkT`-s#?YZQT9&UV^x&>srt*&yl`HE+45Hl6!rcX-E4I*EVMN3_@2S+Iw4$Qx@zr1BSZB KI#pWskN*eT$+VsT literal 0 HcmV?d00001 diff --git a/examples/icons/scatter-plot-32.ico b/examples/icons/scatter-plot-32.ico new file mode 100644 index 0000000000000000000000000000000000000000..698eaedfc3b7f64f6cc4c3e372ca313a9d0af412 GIT binary patch literal 4286 zcmc(gK}b|l6oy~oA|%9xNQgLtiwKLbh=_=uM6`%-5h2kcL?k36Bt$~OLWNp|tB97; zLRv+Hw1`3!3=wP5LPSCcDk4Hdq$0e1|M1Rmea-YnozaP}bM86kp8ws~eWxJU#h-LK z(BHvmRS?t!L9ibP1I2v#Ta4dE^r=n5Ea<+>LDN>Bfgbk$2avGUNdIYVIX@k{$_4QYh`X}Y-mrMUaOs89Yhz-Z$Pm=m4 z<>UIS%U~s@GZX3WCO!a@pl7L!(7i~K%9&@Mo&Qdxp>r;R|DMNkZKkFZR-;-y6VrEN zKlZHC*KT4L!1^kGB;J)+HT(qsIkPi_comGoH_&Wri1&!J{?U)(>VFI>^6YCQ=WEL! zv08l1c0d2da=xYJYgE_o$KyUVx)*vcn$bUA1wU>F|5dFv_5THmfco9v{YH=lT-J~f)RI-lR~sr4{RzTETmZH$2C+jB@;&bP!1 z(XXH`e%G?!J~?TPfu3;zPWc-D^QkrcTM2dj)Y|!%@U!0p{(Uc}8lBmH7tO3+z$<9+ zbyydquRYZ2duWB)SPz}up3Q7^w5NgGs+ZT@Ib{3s3)Z(iaZ8V0ufOzXAybCE63Z!{ zndd6NH~qeN-$K{)(6`wK)1aACZ+r2Ng1r;7XS}@Tlj57!w_f=yy)`p_zzPicb*zi3 zPrtLE_dgHP&B$T(e|oO@WBND!=h}=<|7y@V?LEt`yYKP7tcQ3WIzI>Y)b{^TC!1Gcj*T3@Qc{At53WB0D|Yd)e`~ zKiggRCwt&>&zy7Se!nww&bcGU^jwbVhaA&V^B_amf)!Xj9|VS55}kgMaYgqpTLemAHgy#?6KY6DWR_3!zw(BRiUmv7m27w`a%V@s&3Q>|~MrYbslDaZ76sH?pU?g%#HHLS#Xe4bgxBxbyrX5Z5Y969KgROtK$Opb%8Tpb&ly}e5a}NUvf+<3iq*|@IP?|LR}rl zm?kgwV{NFbBVFK(I$@6KYHTe`i|``Wwd3u$1^3`2o(Oeyp$iUmwZ8?QA~QXLp+2z- z3V^d1%P|diz&WNxcpG@cv=Z{bq?e8m1DZMTs7Xn zW15z{2D|!wdp1e95ntg(?8OoMh+A9#BRQt~LS22?jz@dukLH+$LS0={_GKTqs&wR- zMq03=p|1XECw^A=mlohOySV`!fp2^KhkC)gNmC@p^dNrIWc`VrjPK+xobR!nD)i^E z8-HucZbLt;DM!#YkT*@%?P5)n87t#LU7gM`JyXPvD>`1Nt221GALykBlsA=;p18#= zxrLf?T#bX+hEH%EP8JuB8OxL%2z51)WBLeB73PoI_YOSW>UUyEsH=52J8R@i5ol!- ze!_B1=I?IpYkJ~($CpK5N@lk8{7#?vV<`9e=eZJ??cZ>d=CT35;>kf0m{!_Wx*XTy z=1^CM|9{5xGV2>ro9XA~3fzyt9qrYLi}Rv4M=r-VQM|k7JPw8&(;u22#(YQ`*4$dx d&l7=t{{bX6`@6%pqqYD5002ovPDHLkV1j>Ep@jed literal 0 HcmV?d00001 diff --git a/examples/icons/scatter-plot-48.ico b/examples/icons/scatter-plot-48.ico new file mode 100644 index 0000000000000000000000000000000000000000..1864ee6fd218ee1b25cadebb79ba0625eaeef350 GIT binary patch literal 9662 zcmdU#UuaZE6vj6u7ztuTBuEhq#ft2L1Q8J_REiJ92N97X;zJQ1M0_YdNGL&4C>25p zBBh8FD@95W5fKp)k*$?dL`o15F^f%GNhJiaBCW_Wx4&=h%+1VZH-FY_lCB)jnKS2{ z`R2@-xpS}Pa!vf{>dNtVXKvuVT&^RR%dLk{PH* z)lT(ZR=;#9pIT01x)|1Pc4Xo{g!V%BN_mrjDJc~UBp%ArdA+wwlA+9Ja(QCBF!tNZ zU@s((g?}8Q$6r16P~PFSd}?_de;9YRWOKvf&+?m$(!K&+hAu%DtI!43>H>O8P9A{& z5EA|8Md&gv8TuYNC!!`TM?O1F=3D&Eh|r0jCcmfQUt@m?eiG_~x}i?;Zu5_=>ZXXz z;#zxUA?m@L)eklO|HSX=$KhX6Po7!79Jy#=trxSV^@OO)XSV8xeZ39rJ>=jk=5Yvm z7pe=>Oq>|wt?P#vJIMbN_RbrfPqbemU*#FGb|zTrv9QQRg2&*9zuoXxj)GOcp;F z^b8aJ===n48@12}&Y$Vm^X@cQ^Kc6XuKUMWns@RLJ=e@wBQS?C!BR@G+cl5!kG_L` zJ;zkjmV^)a0klbRnAdvt>|LkN@Z(#?W?t(D?A(vc==l7yI@28bgdeWj+D{EWgWLMS zo_%bJ`0EPU&>v&Q^^;HcZ;H!&27e=5eh$7)&|UYyz{#1Pd(KK&NS4Ak z(pJqyd&m2~4JUR~Kjh7hfnK>i0=ZdF8yEjJ><>WCiRu0CP;cAlo6#9J z**L_{&L85c{<9n*cQM|e&%HP{T8lCI_SA~280397wtAOv*tNIqJnqo9uF@B~C5-hx zv5%0OgZNIX_VCN<2mf0>|B6pv3;Etj%y17pXZ#*?%PKMWHh#+>&MBik>~rtup?ci! zZN-VT6x}iCF{Jf=0X<|6J43r_GIme!_hUr(y*)NM`#&NtyWp$5z81%(1G?yIEvO&A z2Hl}Aw)?RC2X1i$_9gUNL)%IjpJ8y>x?sLHX=n9QX*=CE*yVcb_?+~u$KMsN3`#P{Wx)_ zJ}Z=empk~h>xXexhHDK;L4-$ z*D{Ut*eQ=i@0$|n#0scWsSc=cRUL08ZQ1u_zq|**rI!}_n|M!zt>NP zv|nenKPRm`ioUJFei{3+m-SLDJ%wwnPNADnPj>$X z^%KUKj!fKDVADP43rJj_FQXs2Es*wvUuVnJPWO)K^bY8#v*b%Kzpk(^gOzVWX1TW& z$0n;E<8Zd)<*YGTu&bZ-z(v*4m`=JPfYzy+LzPo%P@s`3OJc z=h{&GS<QjLXH++PeZsQ>{k-5OkEzh0IVaCbsy!1{o_tn=nNv=3^7-1%eo^uR*ETMYv^ zbtlS0dIqe9>^ER|ia-5+bH1AILe_!*2IT9iZy{r?h7IgnA)XHj|A)b>t!A^3b-e=D zR0CmiF4vyuSB?Mc^glY@8}k#c-RyXivupY*wQEWUP26VFYWazB>+g1)a%<7?Ud#Ji zQ{LKcxoJ*pKm1Sio94f*ziA#$akdtF)(G!!#qM^CyOIyTPW4II+9iqkz}h8i6fa52 k?T)ur@}FG)=a!pxl4tW%$NdN!< literal 0 HcmV?d00001 diff --git a/examples/icons/scatter-plot-48.png b/examples/icons/scatter-plot-48.png new file mode 100644 index 0000000000000000000000000000000000000000..a387e3d6793b1e013f7cdde3e8f9a37acf4b5e15 GIT binary patch literal 1995 zcmV;+2Q>JJP)MmJmZ& z!Wu#d%d#x@@xwcF@67DoxSM8gkIJRPOYjQ=BP z0EdA`0E8|9-}zV(tx+Q2UH}ZhmX8t3ZAqgTSfJAPyG@*A#lHb@At z?@kAtWZQvh?43qq5^dl$C)wK#=imVpD0NqflWYQf#Av^yD95@LXxYA61+2Tz0pJnP zqk_n#i-xU|4mN~y776gs9B`}CK$VqB@d?%evhBplN17m^>pl!gn z4d3XtD!KH8&EISEK_L?TI0_W%-5wT&9u`atpaj@^kw<#h(|Atucd_3DUk2Y0oC2nS z6BP;kB3%nIHf<1?HNol;;KttqxA?74v0T?x=2;{~{ zIs?qtA5j`#CD5iKkxiLG!x3>FcwKlw2s?xip*C<-X}oGvjj#a2rI6BQ*GcCP=%2=4 zk~C&>3jC_14+=deS!6RRu~3UleOpele-knrz;UJVXNTd$tt78|_S-&$Gzq>gG=u#! zwVQKg-39~-X$X74t!&hC3$s;X*5-WFZdUm2BpU&~3LF8(11e1d(?|^`ogXRfKNA7n zp@2`a5yXtFUr8JV-jMWylg_4^>K5P$V7!Mb?9q+D<9WX5Nxk}9mul2WHez^D@J;8K zgf<(dgvOm@UoUOkabQ~!og9=ywT!n5jhAglFq9~CkKo}_wCP12U>`76C_04Dxmt7z z7_B*64YBn^wq|jXZMMBf(x-rb0q^87yDa0tXqghF@kWQlb^}0g88~71w4{-eei!qu zum^5lGOc?Df)30Bavjdm{-pc`Xon*hXm;jz8 z97ZkzN5Q`sWIaz@@Bc3p4U|Wz+JRve5@AP!3NlF%wTPC_^7ou%-v>sN#tX$gCs_mB zGCU>N1G-`Olk8X2ZZ4bksslNDR18+_hOQynf*m0dR;Y0i_yEAIYz(mlSO6~NGrLeR zWltHsEU}ZE)`ABtebPxbtu(G=-?Bt`oXY>KehrkyOM=73vjXTx;(6c)z%?h?1BkbP z-vNIBX57lQE;q&#h=-`{Nx>JvyKjXfEVFxkp|#5IWo6(ek)~`eBK;IN7hbGaf$fE| z;Xa9bon%idjW3rx*jesL_4kq|g<4LsXr*GV|InH#)Z1uh^G{E@t@EOv z7F|J9$@J0^)!E1JM@feWA$E&`&K2;pYBx{aVy*^Is9IL+uy`JrEHbC?xTS-F3&5Kz z*u9DQtHfi#<{V^E5}d)DDJw^5eA7wxlJP0feV7Hw?ve^xCG5Pb@PP@ahuIrMBs za@_q57G`pDVZ=#i1hfnI9N~2L58xf}>sY^)m?F^XLX%IqKmoj(j`Lak(C9j4 z3grn4I2XWY5%6W38Hr5}McqE^y%Go1j?c1+lHhhF3Y-Wo)Wzss;ghBAb+OOez;D7= zPuV^oys6NU-9Xv}IS615CZaUHiuoGmhsLK3?<2heew?CEL-$L%2Wkf+ zJF@-QdONV3LbXa%?dH6b?AiP%H;O&0cAv+_i=b|$7AnTLTJx-v1C>NJNGZ>_*9P80 z=M7gdlf5m}+fF*ZE~rZq02dUp!E0-7DUB}(oM$3m3w!2&8}|gKBdjow`+k6rS}-R zsy^GzJ2p$0E}~2L@#8`4UX7-+$Nxzv)W+q#bB$Fgbk|arzBuUb4lO#!!BuSDa zNhZn6yiNKgnPf7VyiGF6Op+wO{T|QzKIeYl@B4lKIrq8mbIx_0^L)>9_kExHT<5y3 z$Ln>j*L7XzI_I97`w##0_n*0)bJw&s&TZlSHQKNWt?q2sNB^0C37CKhn1BhGKpp}Y z{?FA_Xg_)lT|!qaGq4ZX2kZm(0sBB@K5zl{w<1RTv;Wg^(NXjra?Cda`+$AGK42fP z4}|3d?-8Fj&#DK&u{7S_jy^=TwHeq4>;v`z`+$8QTpu`3tlo>X0wDJv={4rR2VF+C zw;9+6>;v`z`+$8Qd>>HEz60gD9-wsSar6(ez0JTrU>~p#*az$b@%VsZ_p@lCK7jT| zFCp974D18;0sDY`z&;SK4?LZ<55Ru9-mh`L?QI730sDY`z&>Cfh}#D=4{(EG`?$pb z>~lT(3)$voU>~p#*az$b_JR0)K=%iZyf1+7H=%Qm{qFnN2kZm(0sDY`pjJNcG4>p| z7C_t48@6|??CpMg{QY*@&^ELW*av3i19uO)20+}`^#QN-x1Y_*&wL#Fl6}BFU>^v} z2filGFPj74^E%|``h{gDpEtILeZW3oAFvP1+6NBx&H)hnA4apz!^bmI`+$AGK42fP z4}|3ddX{hZUI5;=9RJ<_u@Be>>;v`z`#|k|K==3UNap}}@Aw~9|IGbm+rvI!AFvPD z2a586#~lA_-_I+`cYIv8OZEZ#fPEldANbJm-|^pbef9zSfPKI|&^RAB@A%(1o4dar zZ-3pkv+e8y_JLXXz#oqPj{olW+Xw6e_5u4q^L)VZzj-$Id2XBA2kZm(0sDaCzkR^% zfPKI|U>~p#G|mUEIQ}=z=CitX}Ir2hKYFJN~=hZy&G^*az$bjq`!m z9RC|jN75Ka$3Nz4F=5|Bu(6J{N5}`+$AGK42fn;sY0n_v;=1J@&^B zviOVlF(>~Tx51hxw8y){^Z)f+_~TL5By2n{s~$)XxyJ}<9px#S1-H! zJhnaU1NH&?Ky!WI3UU2Cv>}c2!(JW#J^sZ9+*X)@eZW3oABfur{-!NoM9-q`GyaCf zer;zzkN@MgxzAbK&pu!uun*MF2mT>Gr&4U!^?c3iYkV*Lj#yZITb9ZDfqEz3sZ%_RysD<7vDp`}Hz;E5;Qm#m`F<>KFD`yv?GY z*>99C@cmB%^ds9A(CI{q?H{1`&`XJ);PoMN6kUrl$NjV~K$=kFzn)j5bD>Dv_a8g; z1@))utV#Sgi;su6kIp%rTXmM&4L#SR$vQ{P z)B_y*E_CMvx|w|sB8|1jX(#(^AEEVpH*wrf7_q(X$sPnE4-z|HV_q1(+)0=<^G>ZUjWT^39WAVqDWewNX1WdpL zOuz)nA>jC54rk|L0w!PrCSU>~5petu$r>(;37CKhn1BhCL%{LB9L~t~37CKhn1Bg{M8NSsBx|@VCSU?4U;-vk4gtsi zayUB|6EFc2FaZ+?iGbsONY-#!Ouz(8zywU790HF2<#2W`CSU?4U;-u(5&_5mkgVad zn1BhGfC-pDIRqU4%i-)?Ouz(8zywSnBm$2AAz8y^F#!`W0TVERatJv7m&4h)n1BhG zfC-pDNCX`JL$Zd;Vge>$0w!PrpF#!`W0TVERkO(;bhhzi1WdpL z$|2zRUk+#IVge>$0w!PrArWx=56K!XiwT&337CKhltaMrzZ}la#RN>i1WdpLLL%V! zACfg(785W56EFc2D2IUKe>t3;iwT&337CKhghascKO}3oEGA$ACSU?4P!0jd|8h7x z7ZWf66EFc22#J8>e@NDFSxmqLOuz(8pd12@|K)IYE+$|CCSU?45E22$|B$TVvY3Dg zn1BhGKsf{)|I6X*Tui_OOuz(8AS42g{~=k!WibI0FaZ-VfpQ2q{+GkqxtM?nn1BhG zKu81}|3k8d%VGj1U;-v!0_6~J{4a;Ib1?xEFaZ-VfshC|{)c1@m&F83zywUd1j-@c z_+JiZ=VAgTU;-v!0wED_{13?*E{h46fC-p@36w*?@xL6-&cy^wzywUd1VSR<_#cur zTow~B0TVC*6DWs(<9|7vor?*WfC-p@34}zz@joPMxGb>|;QU#QHldwpFS;(#eqIlv zLx~RXx)1F}+t3EII=1?{ye41*^&vnT)IwJS)NhpOvHFm9P3k}ZpH$r5gA~tiLHD4i z(VOT5I*Cr9)95VvABm`)49RDM+m5W&o z0nU;2NaOY!(c|a?^fgi+^zeF#fyO8=7JJFhIYU^B0zyz8?fOf|5zbVFc zJtrc-xv?D`MvtI((FODmnkL?lO8Xbb{sz5)?nb(vuy&$CduJ0c0TakbfcC}lKO-ye z)(iri6I+nR`mdlL(cfrP+%Dp~Kgjbl^Z?RYf(^}}?^>FG36w>EcE$0(EZEK`XabxI z8_~h!oX~aqD&l^BUqbT@x;~)m4(bQ^x30(8dqa`U8L*}piPxSbpcGk z1fnBA+u`^hoz-06yae!f#eS{v`xM16-#@NTqW+84Eo{ll#OX}H1e!^JcEj<%nWk@C zt?~6V_MhPWpJ-fcjph3*)a6;Ut#L+mg-yT&aucA-_awJ^it=uAdjFtL_c-^8sq>*{+xgOizF|!tsB4j6Onu1n}*(NcZ>ujOrTeht=zQ z>bxKMIe`KCri)|(ArPQ_2s!>g8UiDiAwB|%d+0{=Ez_i`-CS3C$)`!tO0e2zK0o2aJT_F>go&aq_sPSLV zHMwK@8u|!LBY@xNUcaZ%FKAe7Y|8d;V6d}kKH&PAfC-EzK-&;<{J(R24ZMd5EGK|Z zUyBw{Q|JBr`!;$mfY$;pcN2dyfyNV{U2yzwycg650RFrNJ&!bh)E@_%`dQb~_SVPB zu89elz$^&RE`%KaJ^w!oBRA+6__Nl$e}z2uU*7+D6((K>(4cd{RW*Te1ZWdNj{lzj zA7?;+-z9)AYwZ65YSO*_S?4q^!ewU{mfx6w2{fJnZGz)}MSE|LR>NiW1<*8_@fx$@lw@i~suG*VLl$Xcy81iYGvO;PHR)n4Lqx1n}b<(03?H z3~cf~>Ld9YK*9d#!<#@11ZWFFj{hG2$KXT(lo3CE4(UF(Cdc+Hb=NvTKNC0rpLG#T zplSlN1&;q!8^QTcMF1aOhxGft|DY@}u<838o(i}Vn}7+_f&lI1@j%;5J-@}*Dr@0o z#a048xd!Q;|E9L)jsI>(r%~2x-`=mo>WLWa>oS^v2^2>l@c8ff z|KdzvizDN|TKj*-@qgm}&+D~t09V2UOdvObz~jHi|G6#OjD+~_0rZXI|HSeC-DY@* zYiR-|FpNOp@!#YBVTSYeHUa$i7WBR2zvI782@^1Z$p{1<|2_U6VNPtf8f`>7(Y0tl zI@F_syuS|ZLAp-9e#GIT{E9l=iOwNk*Po1m$Ny1!ybEmtvm-#e8S?#qj{k|Nv6Jp| zQ>@>Mj-rRr)95Ai4*C#%hEDeAOWuEq-bZhu6CyV=O zKgsdGe0Oz@CJ+<>+E2&-pe(Xf7;K}MzZudBP6VtvBx@}!P z6EJ~d3D9na9RC|J{>KIy?_ZBJ{{9^OhN_H7f04iD1YSX!U*A-$7v?`YW!r&HqbhmD znD@i^sXL_!m_Qu}&~Ant|7$(|hrO=vZ$}TJ6R7IA-yg^Rq)cC<=aJ^wgIga!nKmHx znTohJ($Aw#pKqx{`{g>AfC=O$K)V@o{IBi!AMP6SA3_>qpGP6bFI^WNzyty%K${tI{IAXRe>iKNT5Hu`LW?No znDjU0KaK828>@D0%CHORzJ-{XmRa^^tG1c*Hvto4kAD0QUyb{( zMT-5gT2;#a;7%iYnVFJx1KwA)U{EvS8 z58vJBHKgn2b&WaSQ#aikP}#Kr@;-nxA5mAvv#QqzFk02z7SA;|0TW0G&=!Oo|DzuN z!*&PKdihIeR_%9)$I(2%*XT}E$=H&7*P*vi-R4+kX>VP#xh*~S{b~Xx&};&<2O-D* zXvhC>-HNo%U-PZAY{Nr6mSW2(bTg{t-azuzdc^Z6R7O>o>nqr7Xm*R|+M9q0qy%UW z9RE`oGkS&TdUOwJS}gjTI=_$h&Isj1haums=mb(ZUWfFU8oo1zLH0#@nx@IO2 zCjr_7$Nw3c4xZc5OQ^}uqt&yGG#9^ihG;V#iahtA&ynULCW()A-}iBNcY2PZ%mDAihfuyq&jZk0#JFoG5-(44d_wivH!|gt1#`n|K{S zzNob*p7*c9`JAT-L_i>EA444PXxtXX_@8oKhnjl7-#Fj-6E+%$m%a`_ryu!hUgmT3 z7aCXAx_qyDA6`I}Jc}`cX3s@50TX}#ZA8f9|KQ_4{Pf(}2T>Epm|5fH3Ha=<;)Ek# z-4Adp(i-?C+!Ltv4sW7Lp1)9qjh&|nm_R-Pv=br6|KR8U;ioy~w^7zu*u?$xJf3?{ zC3Df_yB29K{6|#x^?)Bz$K9wB*7;f?r!oN(sDc3PM9A?!hVj4FnSYO(7}vAbSuyt| zRN0u6y!C8`C(t6wTJG5P(>17l$g#hQ#?*P5Kx71HD?*O{L684or*Y-|NY}Vyixp*; z_Y{l{Re5@l{}%KV`We-29^fQ(QeV`uze*=_-X;(c0osd@+eT914W4|BF zL$UU8RQa=LD8oji^$4d?%yTzd&-fPdoPW-9*vU+w$pmOSLXQ8T&;Q>7ze~t5KO2LW zU{%=~fZp?*a;!y%(c4HijO}`W)-kk@)}MOLzt^~aZvrNei2!X!$nihe`G2^rK^jBW zVGg;h{)^V8Uz^D>OW29B=-$9Sl=mY4o^J_$;w(&{t^{a9LXQ8z zj{o7d2|bOhXCvv$= zAR+>^B_YTEkjMYm!tPTPYOD>hT#94C_UUxmQa)W1*p2Q(IyW@t)|`74Jax~6u9;}A zUuAOKkEm^RQB5Fp0<*)`MYSVG$qU>5HxDVZi9zvQIP=7$N`wCM1#xc}>>OY)D zAE6h}{pe<-dlCGbed7$_?@ho2WM~Nw^(CKcI2#y}w`1DBAen>n7NJHQIvI zA5iSpT>XPca|BN#^$lJ|x<~&Rq-~EO?Wg{O{AVxndcIz;es2OMP#OW+AIJY3w3GJR z@qc-2AJ^B3di~h8|b>hCbR)*+nOxL2yq{k9qmAx_tJd`>dR>E;-w*a zj_nU2UCU5EXH$sva+yq^$pknr9shGU0NgZw)4cDvnCI`882tgSdvoA+l9CBvC*233 zxcvaqT*iCoGqixz?_ETfk!(6hzp?!%^ey@leTZH|ng= zhehCaJvxIN-?KB&n$BSF>CE8A)NeH}a2}~Y{RrApKy`gc6EJ~30-S4(|9wode1zK$^g5cft!wiB znokb)p3W?GuggBN19}DN`n=+A2!8nw<+y;f)0Q@y?rM3B5$ES6BA^MK`BbqBZj)M8U$QhZyhcSU*2ylKm z{^zg;+;soK_YBU$A$$xINC+pxKl_Mdeb+y}_%8%|~dy#$hWV%F#9ERJ`PvIT#y zIAzp0OJmzv)VZ$5(DPnz?)5vrFNOe|H7BJx|GLjfsSlvFH^H|N#aP-$HG$~~aQ=Aw zKLtkktwDDp-K$jB_&sgC-iFPtDKI-}1_Cf%g`Po8@kPyVe1x`Vz~fy^zy$gTaPBz% z_c59DBMf~X!0^70)}HCv?rSII>is7p0AsC1Swz$J*J4WlJN11T`S}2oxr_HUfjQ0_ z$Nwq)0EQdTqo}UeTc?Q!AHc=WaGTP{#wCaGZgdvuUb$)P6_heS+hDr03hm)_xXQUXT5AdDCQx@Ht(BdI$a8Q2*02g0u$LYXK&69q-+6 z0-Q6B|16XW#7kR{~7q!xI^H3ccZ3U^B>k<)p|#@WxftD&SCt$2_yuPb7t5% zRps_5*8lf95-c?b@CB-pTPS&J?5}(NeBHm-NoM>WzT45asA=>5{eGTa2e@~}h--g1 zd`Ig>*CYAT9&`ZRjP$%tt!=&)9YWWkU1$^Xy}UK>4D9If|BUPiOPxbn>!#S(-##_% zv&Q~@R_ly>YuI66{5;aVa82{CLG{)34_$-XHjJ0I&uPuqYP190f*wb2q7I)lDJ zT1#>+(f7QnkEv@c3rLK0eji8oqU({KIpJq}N9JJI%<+GQHiM_mt5=b(Ne*hGny~FV z>aMYa$Nn?)t^Py7cPIJ@HGS;gdrtid53dF6a~7DaLHm&6{d?#%Qe3}?{y>xZkosD> z=5!vZzo2=~SJ3Tf3vyqe&julW#7>U?Gc*i5)wkAMnfl1R?NU?Tt36R0vuTD{eW(Eh zVEi=t3pK^JveZ*?N#j`00pK%m&|Lj(=r!~e()n40=!8EMYoGxQ8Pfc$%(1J2!0 zHenyf{}~$trt8psXc1*;v+A;s#t|9=?3gi9AFhu8jJKj6(4^zGy7=0tdi(;HM$Z9I zf6Wa&iatj26UFATd_{Agnuqxoy@_r{?g#Xp!y$acE{^{N7y_~yj@TuNI!uS#7=S>Vgw=@Uv1=`rltO@T`XLJ|(1Zn&s7FD(}SIGY?dKGCt zU`-S35KA4ghvR<%#)9iAv>UyS{z6rrgJtBcxy7T%*Zd3cr~X62`4npM8k2E8eI7oI z`OOLHs_Xj)k=mOdQD|+>@03aF1fECNAm2aGe^OWQ89O-s7sOpYj`k&t>)cjv)a!~Xj}|z^7oqWex(OnZQoMQ9q37t58jxz+;RX5^t^{WuD3d^e2`6_Hl4W&v#;2{Ym^ zshj2)uS0Jkt?P?rj3M9s0X>cU+f5bvAp9Nw3+4~wwde+II(wx7? z{RR6_@8RKn3dPda&&uEO9OoxE-O|ghF7K(A<`xelUFQ#Z%sdUD8@$*(o@SijbcyC2oTOEU+R9!~RH9woO ztII2OQd^?=^V2Byw&V(R&^Y2|UhzeWebX^S>9JL+Xpy(cWnvK<$j@0ZMRII6MBAz@^Q2i zX+G~O6xw;M`O}l=KC}b*oE&7x61QQ4?@?%e5^^kBgx7r~nrP-(QU2BFcBHwCe^AK0 zr!AZM0H@Jmx;TSXCLx?x}|%? z;0&>a=3=I8W1Ey->ujDHOf!zHl>Y{#ez?xdCfH8nj`xv&KeS*Mg|Xv*!HlyU9y_Ry zzXcsYPoM>)^PsHrU1Lg(m2}=cfV94E6Y^Yt76X>B54O;?m(Z^jmF2Hl^SKPOXrBW_ z*>|HiQPb9Z$ZpS~EhU^ij+DZ`jvvGWG>IMN(O6K#2cdf$b7pHV@M3tPwkg4j-b zSZuQ%DIQ;sbRN8fPNO_yK#lK}PS=l5px2S+8E!>d&#yj*$NJM6t-$oyM{`40P!nT8 z-a2azExLOEDYM1_w;)~f*SVXw{xPRhy-%S71)f%oJ~V6{|0D1K?42BiLg z;`ed%DtZUWmc7!pSCGaKx1)n-AJUxSHl&#EHGL7-Uq!@Lhmhm{WU(;T_@B=E7d7t} zOTR$&QyaH6lG8M*V#3w&KProL?;liuK=FGEQY_ywMCvm*&PQhBLB++6j{lRze#N-x z;y-0xjqXFgpjhH`k>&gm4mTsuPY!Arrq~8k$Nwq#IjIShMSyel0P^^MvX~fS{J$1E zzmNV!MaI=wj{OfDG>=(}=Qx$sUS!D!rjGwvKzSb%sF(of>MnE{#nNtvP|iQq##L;( z%-K^`&FejcenTPf3#AN;@HmS6On}VZG=4{TI{uHxiFRc~>D@9AfT!bs22S3^1ga*$xw;ND^N<4}#GpRd|> zS@NfxtB}_9YOI^LzI9F~F1KW1>3y;hfT!bsHdNlr1S%)M`T8~T*f4v1ya=y{D>qz* z0#ME^=tZRK-F1%RdFv*>e+F&JfYZBVAOK6p{|ubGiwT54fb;bo)bxA8CW#;C;dOHe zwi{Ol%BlN17SJU1YudhgcFDeREd6~p0(LjsYo`9sNjP0slr5TcTsWQ%)aM-ko8*YDYbFAmtE-T% zfqISD__4Cqi{G0GY{ffK&O4FXydwHRbvvdw-Bhv3YLPu09sg@#NmrsN1UP3OMP36i zK33}5|IX0;f-U$IW7D)2LTv5{^#K4BqmT50nS;i6+eTt7OKcLHs)jh z0-IRw``=4h)mMp`ciClEe<}F;I;v_TvgQlJP~*R@-T6MxtcLV{jUvFg@euMgfWa|S z{USXlXo}A3f6l?|hM4ReVp(D6_#dKSTqYC9K!EdiD^gqN z_|cg|e+m{+T=!4SOL-qe(~O0im{#|9+?+D>Yd!)n3_1RL{y(2tooWaH{9V7XdI0H~ z>l5fz^e%dDi1eMVvuliY5NX|AF!4oYLH8ov@7}~Xkk(nxggJ$xdM-TWmHl)tM_LcR zW@DlC5{~~v+PSQoVd(gu6_)pliU7X70Ubt9Bi%1_7X5@SqF<4&75{<$93x$uRePg! zO22^KMvtJ~sM2fAm>2;2Fql?2rOBaQE0LAn;Kc|yf+jgMzFN3J-lK7-cYEuzoRV`w|7B>s@^UZk?* zZBJuPCp)yz+Lay={uavp0BUmIEsyWX*KZ2nx?z_Ph9Sp)&;N(*KEad@KUUno34Mqz zAdTy174OHjPwHFzj?SUy(GFB9j{poqj{hG2_u0!IR}#R7HTHc2DefyC6y5d=JHGlGS5Vpy(AvDx z`T^v%7ODTyl(D4hsW@=`O0S4#3+2BPX`X7BUwQjVHtO^3jmOGimK27L|6!WOr5Zy3 z-`#?qLb|SB=6HWxdz|(IPEoGIXw8_a%_k>W|NAuKTXUcXe$))bZ}$luhHhXVIns zRVic>dF?=7p*r^gex`o%fztX`g&2#2QU~2vauSstLu1bGIJ~00=2Mg}O?q4yh8+Jr z|39fgYra4Js`0JXdR;)F#{K?wRo4W*K-Zzd$ExI^wGg{dUHbr9`=x8 zZuBz&1n||JNbRK7d&f4m)m(u3EKi{IMI$7S zI`#o%bIti{zPuK(Uw%Mc)Tez0Io@Yupfx_Xqte#a6n(x{b$l3x9REH4UzKA7mM?y) zInvM3pD5OLQt?aID_%hxi$+Wy>I10H^cjk+ztnGI^`92d0Tlh5f3NMSkL+|mirKgO z?N#LGZ(!x${_Az3obTc2_@9%VlNC<@|5V#}H_|z;7*J%pQr5AxR`Er&p=g5SA%2_D zaTF^*#l?@2=E)1cep0ke1|6Sz-GY8VW%*#t`Me3Ior5@f+hhdb==eVwGVfg?0sQk` zbQZ;Otv_qKsy@KesPJn6GMDl}ccEWV)-r~$pVm&ihSZ0t?R+nF+lSQ034wXYWzam< zgQzywe@bjlS7Zf8$Nv>nydeYv_@=J&evRrD`_uMSvF#~TRv%yuc`44ELTMR7eU;6B zL~82`pC_r>->Ks|^dzdQug}WQeuB}BRokmB`NPujzb?kCQ9ba>>(Q5}?(L!G0m|$H z=z72=bU*qT&8l6V)p0bwTSU*H?Wi&1JnDKg`VP&CWgU<65qz$xQBzlGRbUx%{P+ET zm3l`tnd6hX&Z}qG#CpAY(soyUfQL|N_cM{#TC@Ybi2g*Ambr-iwKh_7{nwzZ`a{>NRfi)4Bj%6Hwf}gtC@*misC0zlZiCm8Vn&BOS8DFI8`JAJYA;v*cTsV|@h| zKl6WvJ_*l|xzXEuVQ3m41Ho+NW;s;TdxL_xQhV4mYAc_~I?- z8>DkR#x`o&GV6MW;$daiyp=cFjE*AB37kjM)=T!hg3h9sknF5H)aR(xz5FBGJJVO< zfpjgXiGAQves%^X2T|kuh?yKI<4*7lIsSY6pV5x7?}i_4LhmERz)|f$-M`cN=3S`L zc8>g4BgJRgWiPrDJ&WE!ny1hhRr9gEQXBmRdJ8>=9zZvtJxK9>BhoteTHeDyt8)-O zyOB88-Pg0$PyI)a{b%K1J;#A-$noFf{~lY_`7M6IcpV~S--z1oO-YCl| zq;aX{2Gl2zG+(cH-Y;#}_lomMqkcrCeVN$K8}UMSAl?61S3jSmUJG#W^Zp8OWVky1 z7r@nr%uWCw+=sqIihYx`3w7Q1FF4$eJU5=*jcTDkS12}|EqCEanmo!>0;(6eL zyc4#L{{^x2VaF4||Bj;XP+i)qS=Hkon5g~twes<9RLecEy{@0!jebJ2@~fDRqrT4p zj`u_rLU-hB!y&KWjD1`PV#4`K}8=nJJJ8H%G%LkD9f=yWq zpz*-7XmiErYo9%gLyrHx?o(qA#P{^9AFb~RG4_T~CbjnuBCma_@p&+=YS>?E0rsO$ zPzZimWEm9uk0ZDJ;~ZlK-@`fN`0ugL3~U_YA#{GDCbVbMwv{?Ry$&!$CoZu}_<;HV z2ha&LtxtuTUj5w{k=h1d^Dn`_;2d)NcOS3>AE+V={O&g7b${dAaILj34c%@TIbQz=ob`{zh*7VS;~C}^4wNM-VFPYKe9REH4UxpKuoCiL413H63 zZl^*k+q>}cvj9u>)tIuwKh~jJk>=L^LLvFE;=X>*XgBhhKM=>l7LNac*rF-Lz~>zQ zCvTf|o(A)qz)kVh*y^d+iq@l0`T@Gf=XJCf`TA904uwrZj{m;?ADF+)qB!{6P3UVB z()c9wvS^Lhrdja!G3rkMUr|3Goe%h&_gX_*mXGWD{yC)a{ z!sl*BTKij8dsLTv{5b9TcmP`UBh1QS=;o3w?r4BK6UK zLf@g!(fjBX^f0;}Io=0i%e=+Fh9Sp)U;oc**nrdFbB_Ox|12>YDNwbJ_zuc=x$I@8XbJ&Biduz=I;Omb6HM42x)xqa(M`ux<~+O_^BaKFRb%~|k=C4DK~?(1Kjg3L3z`$qbqDtY>e)0ku2%T6=x*5~*L}XWCTTj5zlh5WtTe|7XC_ zhsaI`T|8@Ki z%qDd&4nDUBg*d(oIp#b7vo&?}4eF(9{MR7OafEDx{<2-9yiX(FM_5P4Y(SmxUB~|h z*g2#s;&U43eT@3sQ-2=L3wQ%em1JmpbV_K|L?PeSo?+ zY>n%IpL+aXWBZ0$HT>>c^d-s?|C+qd-wCT)#1rbGIse;G-C}>QkE#!#{!J}x7HS*f za!pSF9}PABpP#;VK0>Gj)Hb45QRw%2^|tF(yw{reTHN=a*nBUqv99&YYJbfGoI?9T zJ^x%T6A%G>)bZc0PzeEi?S?G%x#U!K9~T$8ESvr@qe(%YPA*ry9b>^S=-d6?5DB+Gbs9J{;3Y=VbsKao_@FK zaa6l!H`J;jYG`HfOUM6)+CGG8<9~_`Z=yd@Q(}9TdR~A>^v}Pb4tl=NVN~^R_+(*T z)IMja|Lw@@0YhkXT!tVTpy%R! ziDKeE>$1wPKS5p#P(=gdJckm%CmsKX((xB%5Woj@FTnHY5}I|}8{_fR7mEIQf7C(u z`RjKJVzj}m%bZ>Z@EpJdjS9Y_wn6ud%9qwB+Q93^9_h1`ey=%2r7`OxEq~7O`ikMx^dk*9`%}k%CB~NC;AD^I+n(IypyoFw)nFt-W=eg>qUy+isxE0 zx)0rpTIe10F*=3Lq6_Fp^bOKB&9S_Pjw3x^Y!BLubS(8B+#l$34VXCo_c8IuY6#$q z4jP-quNV;heg3I)P?y!{C@L$C#+=VraJnI7SGQN{rgL{Ax(8_qi~<*1X!ds@$(l}D5JlOL-OSgSc^>Z3WJYmnB|X&fE9jW1FMtwpNE`keG! zO<$>(<9`anuR#;QR}Z7lk**`gG@qz8P;(c$E`C3%<#>X6ZAG!S_hpv)3FT%G(ueSo`B9sBH#|3ODR7p|88=ioM^c}2z2SlSrP_v?D^O{kXF6RDTReTPt) z@iX?kPQvTDUjL5uJ@r_F4xqZl{?x}VQ@01v##n7MsNB@Y@qZ8)ZwrZtht zFeYkE#~p4hsXc1opdg5mjM2|4&8$^ zmwE|R*$+_rr?m;^&@*TYs_mG+YXsgvhx~l{FqwpF?fA;{Nk!2a3LJSOIy& z2I{N${{(7E446ed<)bf;z#8;dlwE!Ar_e8G7Cd4)##_`m`tzJRoDS4>p8pT}><_bW z__E@V*4b%H@;%a=fZADIi=S2dqGSDuG{>*L#%Jguv=ucf?x$Wry%h^uXjY7yaGYr4 zKX%qN|GUuHhT2?X3(a>$y%vD-JN~DQm|tr}0N>W!vHR#)skj^0Keqc72^L??Ou5WSDy zKrf+3(XD73iuQaj%2+UQ{GYt<6J`9z)@#x8NPXg2#?u&&^)3vy6?I+}eO$`!_+K<+ zAKwIOPJnjA@qhAI7+w6w&a2Q(=nRT6UeCJBvf*tg$~i#F?f75ww%OG)fuae}jyV2L z9uH%T{~NLOYe?6}W*tjoJ>Cge>@4~mE8_T++ws2$us)^8E0h~mGllf>GFS5a=Q zD|s;%H=(Q=`!{*b-!LE18i@N)bkF&q%#Q!DoPaK;2?R=jc0|{NpGCvkNN-=sM(atR z4%DP8ibYvn)to?M0cRIG1xOLBjt7cPht4g1WX_o0kwO`@qe<|s91VW zF5o36qMVzM?uF64u}zHcS?c@&Y_^tcnyIp*ypI1<5p-e`Fag?;HRwTsGt`BSMiA#h zM%i^UfszQ&maIdsI8J1W6KCLbLrJE|oegEwy3SW^(@Zu^`@Q-d%#FFBmza=pI{uf) z+gX`FT?x>ZC|2Bw{y}M5=+~7j#7Dm$8N;=IJ@e;X+jAm+{s>-C+y_WG9sldvAiG{B zP%;6vd+0~xxIdi1Mff~kvT1T%&R#WUM8JAqID zt^quQ95045h+^(vK8hW+mQ8EgZO`ls^t%pwVmQYsr{jMNhH@EAAOZrkC%O-C54waL zGgdOt{Sj}Y;OGA3Zj|$S^aZjlCu49O%!0oMfHFG%N6@aih$avV0os)HNNdJ)kGtc> zat3j0iZz87Gr4g3in z5B6XR{w?Lb1$~8j+h)HX&*MIrtqa}2l+ok=(2eBsnLuO&Xje1`uoL-SfKIH?b^rI# z_Lxjbc^&`9$NEA2A>9WX{BHqMM#uk{PDq#41R^1z_76Rbv>tj;8`+d?S_knhIvR=T zy2Yf-j{o_5_eR*Q?t%>T4P|uv57ag;mI=g8fc9l0(zv(D*8zT`{x6{5$AkU;MwxZ} z-}C>I_W^X@gxCL1&Mh4i`1*hU$>`4}5E%irfkI{t@b5SPUSYD$2%W)pe~{fX)v7Zm$7*P-7++!C`nDgSQt z5vsGjW?i@M;N|E4Pv3fr@H_t2nM= z|1aoe6n*TMvr&(=NPU?qc*dINGw_P#_Z>ov|5`WmRrHOWD_{a9uz~>Rw)z0O(EF$^ z>j4z|7twLFc}1lo-avg?sIK!hv+iT)8P7MO;P3sHOH$5I{syj0Ex&eh4CqncU&1pq(?Oz@hs9kkVdHi49 zDes(3pfm!U>+8_n=nRT!O@PX(GCzW98T;h~)NLDj2gTSwE4z>IXiUC9SwrmCIsTW{ z`Z^;MsGb1r&}y_7y@wW2$gxK4)5WB$hj|VDIj_@LSoK3spkGnh@ieCVbYJU16vefF z`2=Nk{GaCJa9R^EfteAYJ<|06jiJ;og%)qrC;1&MAjQLI=6R zIFub%W61AsxIBcSx(-Bn9sm2C-Jeas1S%&$+oWp(`%o)sr+(&D?QB``K=b^XgZLV~ zj&4NJ%dU z&h8VWw)PiPL|;HL|2K3FeTJS!`%z=#erh-vXb#{xBtHG|ugRa~GmoPvp81p7Q?HcS z@xK>{-Rqfsv=zXMk{}4Keq8r0?ft2tDKC6-DVRS8~uJL@5dMQ>t zi#C>kf96?GZk->K)VGX%9sg%;X&=x8>Q8{SY%SV_u0y(Be+#+`-HmQT+I9f#K~c8Z zRa+5mJJGx7FH}Z62ssap{dM1JliU+YG6?P&W-I`M3w{Eq+e zn9C(K0TVERAP5jUZa|--CSC{lihA!yQQsFr`5pg*IGbDu6EFc2h=+h;AJWqDXNcT=?9o)aDh)F|SZZ zt@qzF0!uVsQ4h!eXsqRent%zIKv)EbAzBOYRG_hBmVF0Z^ZyV<^^E^f-V&(Iy#9Zb z(fpkWn1BfcM1VN56}^CdMYD{#p&aW^>U9ELhoZjb-|IHi#pC~8AbxKGCSU?4FfjpQ z$!_!}(r+Y&61Qhj4&CGM5z>ABwTb;QF!gc#pV-r#zywUd1Wcfx0P#d?;@?4frt&Oe zZ`H?8U*>Ie4XRb_m+h#NjApI?<@2es>MBiqkM!qI{uFW_e|6u_n~(0lK&E9C{rcLeW3lCyT+Tr{jMXP~OJ`Ouz(8AQu7R%r^8W(mix~eqJd3 z06nuv<@p%hjcPyU?=>Lxb^PxI;`b(C0w!PrGbcc-S&gnmk0V_ZxPUb7uId{9@8o?N zJ&AUpTCVXMWk~Am_&*AezcT?7FaZ;o83Dyvv^C3;MN3x}QL6_5VPZk-k5V zzC%j)InvyM;{CJePNbOM*s=Z~-+_nY{~$2lW&$Q)0tFJF?bY@0^@$ok7EMvZV-4DY z4x&5IBj_pgbdR3n{fp>j^a9fN_oLg8((OhYP~)!YryR4q!pHGH3n=en0w!Pr6A++n zU5gaQ_M^i{vFUE~0MZ=vqewALu}kA##V>t-JJMMAI;8ZDV-s-quGtCTTR)?!)?W_G zSNDdT%3cfaX#yr-0@V_r&C}2`Vhcx9RKTMPS?Z)OrT%_w3%A7cL?d)z2^2Wp;7U= z$nUfUUhCbp9!~v$jRjZVhp!O<{KfIVM)q`7Ouz(+AwXNH_4+%|L+B*>3l$mLCpq>X zl;t#f5^3F_?{h204Qh2%{KWCURu*+-Ouz(YO@MY%WB$YF)j-?Kw6CD`g`c21P@|q@ zKWm#e;dp`ivaX?d{eKf2&~-F{fCVbRWF$fe+Znsw^75 z;qiZ!25{acU;;rBplwv_--})ew4EH+|Ij@Mx_?k>6?~sSkX}}8p#t@5kN>N63+HYE zjV3_5sC)Xfw*PJPCmI)PL;YUs0zOB&UvPb+jq1v;Ab?MJ{J(;VH<*A4G?f5tqMqA# z5Pg7Rj{Rw$MB}M5=uYH5KvNxiK)vw?$NvFbyvYPipm_vn57XHH5$_c@(-<22tNQ@W zbLX)&cKjcU$UB&T2{eE}68jJFTDRDr_FdcuXn-G2QpG^yzt-4(IY~XduL+od3FIX} zd!w~}y59dWs?)eXjs5AX`v7^Jq=xD6^$_R(^<0gQYlz!bG653^kO1w?PNcQ+b&mbL zeSp){^%k@`Ky$cAB@w{>A;y2Lxp}`N1kT0;OrXI8Xlpj1$I)*njgfVK)fh{^AF#K< zrf+=JvA^SgPX!A7oe;ZXb)($ma{bsPPiTL+|8*+wVYmfgMVry5^1e#8O z_D;X+r{~`Iy8p`lkDj}#-#YdjKtoOxZ0-2p5L>&NCeU;Ow0Bzb{~8K)3|@7)7GQH@ z)BU6=^~ctZ|4lKr>uCZFCO})a3LOfx&8s@@r}^s{h>xK)4R)2rR6Wph!O#C~j0d`+ zCQuFm+PV$sVWfNB(|G6C4|4n;&)^L>Z7bd~&Y?;I*x2#EN&`4=6EK0o3DC}MMY^}m zu|FFF{U*}>!VTjLDkXr89set}fHOA%6DXPh?cBBK408O>#z1QTZY$a@K7O?Xu(9KR zwI*=xCSU>u6QG?tfPP1g|JfMmKEQ_xHj58mF#&As_+PONoV^K{Kyd_U<8^~WUS7ElfFvIx3l@q|e zj{lWg!38h@6DW!RZJeI_^B8jcpNzqqFxy^~U3}c?31DBx|LV=)5}1Gq6hweFZZkTL z9RDX{@IK7;6l4}3HUI+H*YQ69JGclYU;@PuppDaS{^)*o$N$L~d;+t5#aP8h4S@jm zb^H&(5H5oWm_Q)}XybOFkCEg5WDGus*?~fg;)4c30Q)-r2Vn^p!URm92m-WmivJqR zI{r__;7gbtEW##rI3{d1p?ygAmfVRRK#!uQ(KCsj1K>h_ugX^bykaC>`S-bRDYXK1=fb8HILU{7ISKkIX$>ToW*X0142R==%Sw z0k(6YwY&Z0`2c3u1ZdLIibVZ2PCtkqL+>EP@AK#n^e^gXHpyol;~Y{SLjL&>x*nDG zyG!Kd_+OegIU^G=fwBqE#%)H=qeIbFx@L4AQd~ZXenomFQc-@P zV=ESaiC#pvpbbT{AP>j?q9ObECSU@U5ulCRh@M16wcT|+?rSjHQkmf@$e8+WN4k#x zKKc>;jmoyK=D)v2;(r$^tRKL!9seuvGiPZ6CQuFm+PHP-epGfl9(#Vr#jYGHRhtX- z)Ev=0NOSx@p(4lnS^YwNkMEKC0f*4)SySSej{mcU>f@P!2?RrcHf{~N9c68Iy*e2sdMj->jb8<1k`IaI}Xf1JPlMY&F)Cy?eRW@ZzP<@jHugZbDdU;>pAPk%=KrryUHp*R=;Kaw1hZ@tlhK!sgve{4kOi1_vZhLVzjm9XIe;po%?zKX&wKA zZn<1I6EJ~k3DC}Y4Pf>-rQV4?wyQ zgeM)4#t6Bh4@8iYXy_33W-=fS%&L*3;F+#(z?m zchHVJ+({Q={J%oEUd~e=r!fH&Fo8h?XjAqejs5D<_7_!;FJM%g-~FL3>igY;&Y(KQ z{?z_yA7J|+zaO;?9-+qn7e|%b-8+pgR3dT^~i6o)5r&j{hM#ughctCNOgXw0Bwub`)vt^q}^$ z$=k%_DU^BqICBHd=z!E=EqWIHf|_Pi&EedF#?IlekK_N0e9nh40TT$H0Bznb^dV|m zTo_m1+N}G-wzr|PsHtl`^o*cg3;U2h_A!-(O!{BYzy#;kv)>?>K}0 zLH+e?;^!~nuwmGV%Xg0d!{B(k379}_3DEX!M$e$XQ4?c>>iida8%6)Df9jy?I-2vo zjGAiC{(8&452Ar{0Bm>suWbwGikW~33?ra65nYQuLjCP#Q$C-j?wTVWd7XF|!;rR9 zhXd$q)YLV9!+iF8m<*f)V7udgNKWjsn1BhCMu7HDV@Ab4t(P9we%5un?h|_$)nVRW z*K=M(TE|#dd*!W{`a^dPI;q(1_&*4Yx0!$mG=cza;Cgf~x`6Vwv9YDo9Qi9~;%CN( z=oHlBdUOVP>_6CtPr_!!91q(Z|3h?Wm&pW7pfm!sgIWvlG}80aVvFUImRG;8`X1Vc z23{8~4g8EUq8?hu@hh5?MN{_Iy^x3cakI_wzaNS}n}7*4iU94P)&lH6$5Ea809p_B zG19g2730AejZQr_qEAti*Kua)$Gi%oW&J%qJO0P$^e(drn7~X4sBJ_$QC<50iv5a{ zH=s2$g;(LhsLNsW9m)~|o4k*%Z!NPspB?`z^j&9d0wxd+0oud5_5sq^uUNS@9OBcL zg!(*%^bCV0$M!6B*V>4Ky~O$K_}>e}?@ho28cl#UG3^7icz+3HXGkA!_D!(7l_}RfC)670PUjs0Nc@H=qw63zK<%~Maup%+J~0S z{no+A)ahE(#CtbK*?iP@-}TxZc8>qOK>XeWOuz&Z0<@1>TXsA86lo59RNETTcbeKn9sn;Fo914kj$Yqfacb^5sd&mE}oD8mq37Ejl3D8FB8A1EeODKlx z%vUJqXXs9}dFDU@Iw19X8pU9fkop9BQxn3@@jr#(R}(M+6Bs~%c5*e+^?>`3Vyo_< z52;O8Sv0oO^KYL-*P(R-s#2>>)bVYkd)q^^Mabp3J9PrMIsT_G{AvOwU;-lu&|Yf2 znPThX=nVP?Ro&KWuK!1L9NmC6ji^FxzM`J%(FvsbS8aos@_#;c0=PN;r!f3#0w!Pr zV+hb@YHi>~q*(hr(!J+ZtUuRy|9hnMeYc{msFv6H2bqMr?m(weOtCkFvc8}4gq`Dm z3d64^U;-wPkpOL{`T<*z`T>uikI+R_WE-!suAX!I3DVj=<-Hx%;#qzf(YJS_&RY9_ z1{KM>j>kTka)zDbe+t8|CSU?4kb?m2sOF-tK}XT^NWZr-tLM=x=3hb@>%NF|KZ0Uk zcO7?*B1fNu`X5B!qB_RtBI|TIH9qVd|5F%#H31Vafhh>ko@$+-<^;4x;8yeyQh(qr zq%r#!NMqLT(0Qc!0!i<+P2XvskCD>o8AJ~w9cw?@iW)c8A7m0Z+=$MiB4cG8kL~zB z$f3N=1WdpLW<-FvvJR951vF;ghptBl(P4B`BE8o(eW!gim!LFCt7A0gx_(yt zFcCY)|H=D5p~wHr*tNSxF{?j#KNBzk6EJ~h5ODm@J?{~E{Qn)>KG_T}b}dc71WdpL zCM7_O(42{`nbdKPzQ{U#p0bAB&jIpN{C_MhqhC$H1WdpLOrVzlF+%Ghz78;c6&br{ zaqPEJj<9q5Pht4g1WdpLOdtvZ#IC*QE64wd=RclJ4ea=zlJKhun1BhGKr94^U3%`p zX~+MGos%*l@&W<&*u+Vb$=g%_rCrglf%2LCSU?4U;=Fd#IMKEMHG8nF01@{E@k&w z61;c(_qk*OCSU?45FG*H*B$5_Dl2}*p3hUgMt1z~rQ!D`U;-v!0b?$4Zk-56EFc2h=l<0OKSk%L4Tsy$L2+sUvvHIdTq;R&;R!V@p}_60TVER zXb2F)9u72)6&=qfJ-+%$-E03caG>1${J&_?&-IZ z<9|O9e>MRVFaZ;Yg#a}vc~D6_InjBE5`mZY@ocn{x6EN&V@7q6EJ}=2oTG*p)Zhs4`68y zWa!vmR&)Fh!*^W@6EFc2FoFC8h-I1scqGu6R@B&j+;NAF{Y5%ZRv!Q7KMkDH1WdpL z8cBe7rgZ@CA^pzHxY$_t??;aPOb(he`=R=AzeKt29aLU#GXWDY0TVERaRi7Vd(oGu zNzVk(?;zca){Nt$@7XWJ`2Q>Ax-(1py^jf)fC-p@3G@>nhO9=1(ATIb_XNH`T{rZz z81|Wc9sh^H@pcn10TVERXbBMCv=-nt^aHB%^?udu1X}To|53(cf5-n(fc%{an1BhG zK(qviacj|isLsC+@GbSz^^URE{Cf?_{*M2>K>XeWOuz(8pjHHkbL-LFNb>-7yD#ty z>a-7yyw`7#K}qBIKM0JsnScqHfC*qms_?KZW5} z6EFc2FoD_a+0w!PrwIo3N)0%*5(Mw3roe6a=V3D#tiT0qivoZ$9a{Qka zOdrPtOuz(8ASwdHK+Of{dBI1~N%R*Ar5~WVe*K28)-23q%-?Gqj_LT{3&ihDzywUd z1R6(xSg7X&>_iWsFOi<5TU9?m>-)4u@kXTU6uH;9&(%>+!q1WdpLOkjEffyRHWN!&ht?R*3iFaZ-V0TVERK?EHC2T}1h6EFc2 zFaZ;&Hvz~0dfV5vGXWDY0TVERK?EHC2T}1h6EFc2FaZ;&Hvz~0dfV5vGXWDY0TVER zK?EHC2T}1h6EFc2FaZ;&H-SLozkX9`YrV<4b|zo~CSU?4P#u9l!12Esc6KdI zzywUd1WcfpK!EZ8AIfy5w~T&o0w!PrCSU?4Fk1qF#{bi^t)-7<0w!PrCSU?4&`ZGa zzn6yJn}7+JfC-pDGYAA2|No{;pErZPYiR-|U;-v!0@V-*F#i8ZnclC4hI2Im6EFc2 zFo9+eAl80`t_CE>|7N(JYiR-|U;-vkApzq4eF4RO^+T?pmnuZ!tWCfKOuz(8p!Nia zwd>F?D3m#X%arB0+JkomO~3?9zywU7Vgkh5=L3rWzfqP)Dn{b$O~3?9zywU7_5_Hx zThU*E^Z|aQ97k&p-4!$e6EFc2FoB8*5N}tZHv)^xxE`$k~fC-p@ z2{eoVF<93D4xv9#8GQi7{?lk%!%(}rCSU?4U;-u(6aiwf`Tz&e1yp1o;2(~!*uOO> z04|IPn1BhGfC)5|05Ms8fNkg`Dxwdd=ls8kHZ>Hnt8D@%U;-v!0>Kg>KI{3w&!Jge z5BQg3XY104q>eYp5(*MuZtF39$|L5w}X_C*)U7ZTw+}yD#(;xYJy7JFYWxlz& z|6QCe|7p|zcVW8xr%nGqt?BZwI{km9EWguFXaB1CRqcQOoznlNP5-}B=09!v`6=^1 zQgQlYQ|3Qi_@4WpDbxRV%JezAAIFwDZEb!%pO?PL_O6{i+pYuma{4)Xarytu&;0B^ zK7aBOaQor*!|g}jvnT0)D%*S^ZC=a=f#*_9QQXY|3!+=@M7%Wu=EQ`pW(!A4cSebvjA*qx0R**f7(1Z zcVy)^eOp@Et#2D2lWEEMmEZJj8InGkmK<+@A?cH8$=vA|sTF_Y4cO9deRbmFf;MM4 z16tZ`X!-%)^zAz&eS0%UKGwJUijPCmleGOeZTcfCGVI&XQZ{|tu3k;tZ*FdW#aDeB zib>y{RUg{~4$i+jt6p{bu_wUb@^@$Tr%gZh1R9fn&h(3O$=|f;$A+-MHP1OKeF_#OeY@3cSD>4I zqVs2w{5xHieCbGmGoG<-_SGD-SwoqN+CX&-vTe@ppy(;s8^gyr}F$Wr>Q&6U2r znS<#4$iD&UkB}fuKQgVpTuQ$0)ubryB%d+%|105Nim~RK9 zZ&$i=v}3u`FV68dw)G#Lf7^iL&DT?YW!U~}m%nX*@%V?PZv#9sw)(@2=@&@9yh6*854V4^8_7qWMlJ2o-nacupL(XaP+9x^IpRD_NDg7b^Y5&c4zwGvdjx25J zd)HEu_O?a6(_hcdNf;b)2DFr-ojwV<%hIRY=G%J=`fot`V@vxj>gfFSnq=u)y=&>~ z_NKI~y?vfVeam0(DKR#E&khUi)Y;Ofw(69obC5Lsk^?UE7LT1;%Avh;n!eZB+TXH6 zH$6KouSm7f+ z?)h>>`gZcU`JP?c zN9=s=?A=?trSwPIyCn7LH9$}Ljsx`4?N6V4?%ky~efxdy!Mmqq0yX*E>tVg=7w7ov zl(bW${`3jq-YWK{Z|~koIXHbg1M=wEr#HRu^p-6h1+aO(on(P``Wup7#_a5v?66e+ z&gKOr=I>bYJufYNPCrt=^lkgKKYe>=_CK<;S9??UW2@&IHuk3{Y5Q-!n_zkVOPf3I zW}LozI`#NSO9krBH~rj~|Fr45?%dB#bT3*7OTY11dC z-?9)gN&Zu$KPLVYr|&vU=Zu>+{o-8m*V6!vtp8H}D*n}O`W}ZLnLZ84$r+MAeW$?X zr*GG8E_Im6^H0-v9Defj3v->n&Ivb3`qq4}#U@TqR=w#v4PN&Aamli*O{&| zr)Rv5HHrVJr1F`x0Xd@N6Zr1}>2=Pf>3PfFqPDjG+8^8hwe)%EWAd)O(~+ePSZDKM zd$ayqO3+&VvGYYITS3#esrxPEKkoePU=zSrJAKmLrq)>MPWQ#{v{;=TmCHgqeaC&e z)*PFDv7Jh_?7U0jW#;@By6HPLneP;=n|@sUXm!#bOAW{y{dEq;cjMA`lDC77-ln*- zrcayAdGc(LKFzM1KH;e{>H8jc(59}}3ELuHC-}PC+owi*&%x=_e3Xq`=~%LF>doB) zvc1Lk{rUH%#{<*iFAXQs0u4!@+NquJ@08xs*)TVs6lhHPrIgA3Dy#m+q+e|Bv}Ct# zZprrH{m;&x3+aB{gvpy$^2L#*FWQ@yKDOU2B)j!IHo3v7wQ)I@mNxY5rcLd?V|`yFhV8XWOFQ=M)=sPcj`V$zI9Au8 z_`0S2`gYT%_FsSc#DRNGpV8^(`wpC(m_6=0D1CbRF7va_kyaL~rA^DT8IgWLdoMFf zOB*t$U)0{qc5n4>=sC@L&y!9TZ3Fh=A6@@mQ0)V>4bY2!!u0KUHs4#_k>y{JfBOtN zHbr_oa9Q;yEk6}lR{V+6|Ghl>tm*%6Z=HMJXXCff+vN8q&z3)C?>*;x50EK;x!sZ8 zR7>wOrPod?eW87jEa}@PWFPmm1X%Sz+R(r#L`}XR5Y=77HO?!9!cck-0=ks6#wDjfZ^htX<*aI`9 zPrE&RxpY1bO5ff_v66;5iCW8=?e4KMq)(z|N~4>!{bo@51+s4cb#|RL{bF*o`QMCU`cwCw*R^g zh(pY$Nm}|U{gAw->5~lGiTSUaezE6$OW*dTPaUpv#&x>`-9jtPu_TS&_oq)&%(wSR zKc(jAbi+H}4emcBqsGMH($aL(x2u=J9F%`2D5R%vH+@>nMHM-PF);mt_DJivBK<=9 z%Mtazy4c<$$uG4=7gOiUW1R!E^eH)3_od^2NfDRQcakJLk977Pn7)%FLDZiCGf)84bQW6%HECw15U z^b35)U-ETJ?|V`%<(YceQu-t9FBg=8zs{bmrMI08?f+$?`Q&rjnxy{NPQmw`uiKxJ zJ$qgj((jk%pwjdtZT}r>@7~h;o`mV!ZuU#F9?ALCPRxHtMy5}Xvm*cQ>6|lt_jH=i zm;c1+m+W^;Id(sG9JnPM?UD@pubZIL=5;^jOy6~fj(<&@f7R(11x$aP6R@SX!_c+2 zcY$^IEh>TjCQsi!++sU%{`3oJ`c4DT(#MhI?>by(w*@88-^A%V2^QLkCrIBt;i^vG zG1$cAPosHa@X6D+fTh3A3A50C&wp9@r@evP>C+rLCw%%Td;V#Pyy+L`I)C$RH%*V0 zJ^!@DI5K7WV>#2O4M5)X=?U1*zB>n*z5dAw)bW?4^jX`V^dd%Q|2*kiZTof1e|53_ zf&a4SztB!H->Ls%`vd=FPv3EXBYo+!#-B7~9_yq}y(u}JS}IQGo1y==4Uphp>W*a3 zzkLQL_?OaWjlUFYfifoeCy)Q6mj@q9s+W4>+fDn)ww4Vr)K z*>}*}?ONOQl{qME3NYcd64XC7&RFsCR?V(gCvPuRZ6N5+?65r*C^*(&+RY zC3E_Av(t@&-6Ld9-)Z)`>C!iu(=W{F??{?1eQjxL`eD>-d&iXkGpAQROTCb8PAzTi zej5HJX*E|EAZz-h)n4I&S<@#`V}${;rcb+#{WY94{jt6SWKF*~*ZDh=GyTGp>03$q z`JDNu>C?E89;UrL{V?J+$-dhSPIsL&eZK={OrN-KI*3nm&)oiY)Au`Y#{9dz_NvqO zpFkPQpPYV6{g#wq+3EY+s*L%kr~it!u5sy)rIzcwPF(+d=@;kHKc`O&&~e3#{8#eB zqtzB?Ouyv+DSr~GEy$a`-5_$Tyy@Ew%6yLWlv#g^bNm5d;`}?wJJV=1#(C2#djZUu zK8YvCvZn90ze)R-#FHZvr%&4dHi@Cr*E4dHVUR`7b8rKh{p)Z4NW1PgC@z@BDs2 z`V>z`OGIek(Z+0!Q;m$?7r=@SR;xL?;B<~qNhF&bUx z+l4v(&G&xbbN2L$N}f(&EVMuHU-tAJlO-k1k-lw!wD;edJpVSpbo7%ay~>>6yV_0P z`TdlnR-<18Al)NRdORqtey;qlE-uZUuzFP2X;RC+Amn`o*OBo%Vn7@^{m><5&6Vm&eb^^Y5l_$N$OGr_p?F+Vn?SSYq=0 zm-oM>NPjF}{>%HHljq;<&R)%zf11A2|IU%WvgNNce#nu2p`AQ``o&KA&iE%s{)q#3 z#$P$oCl1^h|K&)ZIACY|nInDTK*#dhzvBR8+yZ2d{FjaY^Q3PZFqwbIlfG?$`TXT? z13Z>Ly}+s5dHHD_j5_nV(_=x3KSg>HJH7eMIzlAB! zU#0I(E2IaV;B_(b^y&ysQ-7sTXO$+v*4C+cf_lDzH|q literal 0 HcmV?d00001 diff --git a/examples/icons/scatter-plot-512.png b/examples/icons/scatter-plot-512.png new file mode 100644 index 0000000000000000000000000000000000000000..1ce288324f9babd1c27036f0e5a7d1bc58b6ddc6 GIT binary patch literal 18656 zcmdSB_g7Qh6E}L25JD#)O}e1;DpHiL6b0!mfJi776flZXq$LQ7C@4}y`lDc@1(4p3 zND&Yb6o>*6MMZi5DFPvP$M5^D`^)_Y?z7gj)a-M1otZuJna`Zt_O_PX9HJZ$1aY4@ zZsrI#+pNPKiH5@U$ zy|g$xJ3CfqS@&_!MN!mzL_t64aoKMr4%WMxUCo_cB7Qr;el$*aA9`r1GM_{tb*1RW6p39%7-q^ zcld<3rIevfXEEaxFgv}Fe~rSM`^rxIRX;0A;cnKono5({3rw zLd+Q#9>qY7{5a5Hm}Kz!B@d90BAX;}=<0uI5VIGD9}rV$zL+7H>qh_YC22IXO|XdB zR20X28bSZxDlE)-A=gdGc<}6A!NbfEQgs}d^KmrgU`|Epcv&bo*~}iV)1&?4tFxt% z{bm6MV}m4BlYay3S=m2_*cvYp&auXjIp52Jb({K;}{f;N(u``_m~MZRl`L zf*|?9y5Rdumn-ZL7~(l%+vV5ri%>MuJRjdw`3eZt@v)yLM(*_M3nQ0U-*-Fi<~jHA z>}b+hXbA>AJSf>RHEESA%qb6r2H%Ut>IrY|*IC}UE|K0p;O6Ja8_RBRU;cBYLUOm~ zUj%cP6z!^v@P-5lf@)`%_VMLB?w8#n0V07g+WK8BddUcgy!LA=Bv? z@`>8>X|XNW;0gE`{4?T#>UOy}bJdQD5f85UmF(t_)bW;E*P~Kiu%Gio^hKV6s{Owp zv3#k$1j}t5zcj{~_=trlW1;$;ZG)axB$sNjiTz#($OlW^WULWEoq+bS-PN9e8SLZ8 zKtjS@n}3H|JpF#%n`E;)y~nmUR#U2@3gJ0EV(~@*2-xu#4XboFM4bxYY5`mm#wYPOzZbYGt7e2AaTKv;aLm{c)m}f0K(K`BQQ8qL~yy zo*Yzpoe0{2oZ5txOorhfZpBA^y7Xei*9$eO@+a1jc^YS`x#t$zMm9_gj!1289<@XX zGG`u8zg{o(rjhOvozf(EF~Z>4%h=06&i9Y!Zw$j|XRWo^r>67HaIQP_Z_Yv`BweS4 zPmD+9XYy5uNU&+_`JQy|6bX7sZ)%&xyBzdCVLe0U58rdgxz+8{|9z2F*r$Y{l=t`2 zC+Uz}wto+qhD`6On@2J@8(48J3}L3q?7ym6yS21xcpoX&VQblZoT(=9i&3vOCKIja z#!$*Y$5QbPe;9{zQ@>^&_A;9|5c%&l|2a~!0(~-ZjWWvduWz5RyTK_zT?c9=6`1eb zFGB%EH8Y86bHx-g75V78m;!XZs{w}owVN~`;hOV|w!L&%=->O_JQ5ZdB^$9JBcy+e z9EC2>n*{!ib>8s2q-T9P_Cgs$5YL)bzI{zJ3n@op$17P)eYQ>U?k#%@7JQsA zo;P=c^7Bwh<<&V+>Lg0JQo4|F*ZbQaGwEVS`D)iL@20caC5P8nFknrjf*vQcEWX;WiqYM zTmA+ChShvrhAKQqTqT&hs(wnyi||gEm@(p-XN}2~|BRsO5njf*lP;^U-T(EDdFH84 z8*ayG1P7s_Kj-(?)f9O7pE_ts%JfzlDn z*LnEmI64ryE7G*MHWu-~5F?&E6h1Wf_KSkm)NO+vTc0tgk+<-&<-g@T1~#>GF)vo0 z!0kHhZPtRb@YSMuP)P6`%i_X=bsYyFy+SIw*12-Zh|0fA3kT2PrHn>gtwl3j=pQ3 z?{E3|Q2x4pX=Vu;8p?W8^W=$AHkR-3jhvlPgcNJATysy`R}boS6|7!g(7E)U%XqfA zT!lN4jq74ey}C9l$KXZ8Cg!#9PC%i7G%fh^84l#B)Tsfo#fd7iJ9WFpm?@~2-)HSG zWJiYTRIXQw-n8?3L_9e0xK22%)=TKBIK_LLDFb|`b9Y7^-}kgV6=I3vr~FFM$$V{E zO12q;I3wd4yG}5L_?V61uX2lV(Kzg zrxs-j`iNo*8$Y~1@}6QpWf_q&V94qJ`zHK%zN^;iH1kqa>KE+DOD;>)4VSc)=f<1D zKh+?P2?RAF9i7#uLYRE?&>UQ(sl8WbmbfXs41<9;%llZrCg*XnHA=!Z!IS3h#0K&9 zgZtJ01D(AL#CoE=e@046=G{Rf&b*BgXIaOOvfO!uT%-${t(u7plo;Pa4Vr1+1qjkKSJhPmT z2G@9xcu6c%(FL8|Hp6O`Dh1xU!{`HVk?){;zGzZ$3r7Q@8rCIxt$HDgnH`s7C`<%7L{Aux&z(<0mk9oj2aLGMH0x);A( zO<2d1YIgG;G9SqPVii`rL=yO5MRiH1nh}-RqOccZJ&Wh!EJ=~X>0j3LQEw%5Si<4A&xpg+K1dSZ5XMie!| z9Ym#&i*IZ(>3^#VPf+=&iweoprCY{MgZjpT(NQw|nJ)0slg@#o*NH*6hj7cCo!Dc` z;|1 z&)Fn!u6HTXfCm{GwKAUxLai%zB8>Br0?@r5XhkywN~XK9yQ_7Gvu&vJtv3(_8(?%OQFuQz*3 zb*7YY_ZT-hc7O5r%B#p!`3I{-ObaF4wWx`l6G0 zA$_sKvvpXen4u?d5b!yr5$@cjJ!z#E6=!~8fw|3=|9k^C-CXB4vCl6lO#g>~YxCy* z(1F)Iu2FWtq8t~z>hvIFLE;s1bF%V#&yddG6iC1F zV#Q4B&M=u zw*aXR8ZWJ3m@bLS2b<-MushESk|erA&&{-D%70cXRikc@TiHCx(p6rozqmrvmlViS zo0{Zj5_@8knF1ImNp*|jl`&~auef;n7}y0?@xHYA%Yh*B*z{77C3m%K;lA>-#Btc=4~n{Tz^$HMriKUmv0|z|BvTdv3x@e93f#Tr`qhysSfB zq3wPDy>J0Rta?>IsBSD2mikDm^`>D4U5nFH@!ef=<@<{JWqtKxZr?RO_ zPPkuFV-BKoBW)Z?z!^sW*#hxZdcoLs4APU^?OP*tFP@i%9ob{5^!zsh<^aA`YOhFp z6?D&OQMyp)Z!ChcP-_apG)d^{Hh9zdMh+VI)hy75=AG7vz){a?}q)?<2}oK z`q7OhZJIPeohA%lu0VNY@1vShL#ggIyN-6|p)xY705*`btCGcGX}qvLiIXT`{Tqq+lV$z6?TEI7fgA;3gkUGJ7F){+TK}$12~QX zo(0el-CDu>zMqM!ined6*ypc2kq74|Tb^m^Zwrp!O!TUS4pl=4YG&0F!~G1vO;KJX zw7quSy#>@|^^g#IB6BTr4Ze*Nz!o9%(Y45mc4S=gE5_#|+_`YddJA!3L+bWWy;a!W zw+ESlulr-*5|HR>SV}_is>i_8iq~)?hkoydHMMA)jwSS>rDUw1 z0)Z(qA9rZt+xU_L02)^@GXL+2$hFH`T_0lt6O0pVcAT!YNxgZw)lFoV_?hO)RI(wt zat=m(WD5vbCvuIY_J_a;DGbjIKcpWu1FAD27B|_FcT`$HeVw zwsGDj8a+3SMczuYk{Fjfc z4x&vcXLEGubCck85~byRdrF|uBl*uyVEc?&G|;<$fp;xy)3I;d6ssui-RVNp`76)3 zak9@Yfy*_}WzmjC?-b@sebAQ{s7ynsKsf8lU-@cyiO?au7A@PLN0N2@VYFxA@ktPu zD)NnDzVQdHv_W&X?41p95Lt-AV*vbp-QHa8e=(5iL;25E)FAj8uD5MqDN(c*smD9^ zc(Vq?%p%3hCkFg3a56=B$T8wU&}+Hf67uXWb~<|vUNec!_us&WsinU(Azxf(+yN8! zTiHLx6D1mM>>42T{fBooM?LGNIC(J7R{=m~pb!3t2c7V(%8c05jqM!G#Go_d9Dzzs z*nhEFrm9`laBle@sXqp}u3}I*v}CVRO`ztn3w=_c1M&H#2+j`=97ltwrsH z(|aNwBt!DVSXLgyttW>@v@S~q4{Wc&3IBriWHr zlMLdicDdX>l6B?qu2Shl-~f^lAbWkht$K*wq=oYtb1?yK$0Tw93EYJ8KfXPAf7`^j zx6beC1<4ZJ^Zn(I^-9i&Daa)J9O};@)%kApRCGJ;^yU2#9_pVV;$;asG$FiqzCQq3 zD+tmAH6bE;lI2-vW98KS0qlp&MxaTG;&tD>F{e0pj6zGsm3WWMbfrX;wIfqQNO=(t zqW4gwJO)Rv)DJV}h(W(uKA+O@>nGV-1Q>ja)JSF?cEWd$(pJuJeOIbme8`9w&gWZb zU=B5q+5|#L{^DiD3fZ|_RmyZr`-Ta}OH2uxLdw%oG+~rMMeYMlxuQJXG zXYLyDa7gif4)r9-esrn@U+S;=o>)VIFoF9U6V9S={AH!4MLa0`W03C}i4NUy6sqIF z3Ad|~%z2A*>Bk$b%9Q{FEzl$(O+o4&}bKZc2r;O<4{E$Pt< zdVrQp2eKLhA+`QAmGs8Mz3B*C5??w3o0>G!d4%e+@Gkbr^-9@P&uHEv(=<38#8YR* zv+fSne;}nXcdh4@btmMlb?{^od8_3VB z_S`0`Df}iMnDM>G3$bu*&~#GQl@r^p11o6!io}u*#~lP3qFKX5R0-y`-`qw3CxtcU zwqVT(znBy-+KVis`K%n3U_n|Ab1+%>GH`i26knl%1L`=srQ5-IDe5Y&U0N`KrBf7Ms5hXN%rA^wan#ji76|@y%hS&juXsBO|g(LQ`J;>d_ zr&abyuZkh^nVypKN#V?q%&s*Aia8`5Mj$C;>*q0qgYkdaX#MME=Fw2D8ge%KcNH{%Y+@@Mw80p18%PyjwpUGC zJ1fYSdiE}A61DzU^0?_qXoHV3D}_ulF!$JJS5-rCDV?j7#W8sF8+C{a-lJdlROhx1 zM2bhIF*rl}MosY)iDdp4=?cl`R`-u_?{JUwhJRSS9~CK}NA-}y02eIXg9(m*{ZoN{ z-!8XpRC+F2_n8yf>zXOAiDs7F7faKgu}!A za==S&^1cCFcq@6ks?&i(v77|NGkN-Hj}5Ihp{AY%E9w-y3u1DX{Prr8OW6aS3%t&m zKQ(C3`swH2>Acl&5d7M{n#2JW9Xe&VRzlqH3D`vhe!tW`TQt^kp}YRVqnmc9$#-e< z7h(HOB*~rH2K@7@7T^ql9Zn8Z(lR-Z_nGYY@Qjsc*R}Y z+1Fc*yhvT-5{qPS+I$5ul>?15Esr!tXrU+od_sCX`@f*e4Zi9b&+D;3*o~&oDXV=%F>j)R6PpDkjGgj&_|2Xe{GUVN! zf??I)tMiQ|bIX{2$lO3>9dKmBt}+dT>g;uU8alZNo+Zzu%){F=C~CA8?|vPG0Lt{&Of%Nn<~pl zbEHd1;a?|Qi%g0$&ay`xT*cqzkLo|gHy*4qw|3~~IEUz)+6~EjL*e5acn?zqkp*C> zoWu%9>h;Te$6ol&VBUAtDt~q9Qbp*pi?14{z;}HVe^MI+mt#~kVsBnSBaXq`@Dm47 zOvvW}UjgVi7F_2b+?R!giPq%k^$#&IO_hZ|C8_tuUnEpLYA?T=(P`u7DOdc+{uDMG z+T^0C_719rvu*CDU3GWe)&9@Wq&RN8Vb=~n)7dX@v7*i0i*kaLL)2g0?>Q4;P^ObS zAtHZA485!P=V+BygEUovc9=TA_Uc4RweY3}?PPk_YsrnAdq_y}klUdR)A_{7>+K+) z`BbW2s9xEw>R^py9&5}GU|#fYdX;$|QG0$EF`iPEzp=;lGF5;!4PS#p{_|S-sDOqX zuZ|+6P&)Rl1*1k(FS!5AXC6XHTXed=cT@6(k{=*LH>BnuI?za8=_c_)vB`PY6&n30 zF^n>J@SyYdz_UPJXGL%&mv?=c4=!ppE@{D;PD%YZ;hHI@GzIHM@jj;7Lh9+)&bX^v zO({~buEMkc6%&>3MMn?@Y|RpzuIXLHmu9?1ZoOmycg-F0txnh8wDkfzDS{S_$88DQ zs7RfQul%z5QzSI7od1*#UWAitGOo$^hHtJwR!7&G#r&fX7_D0T3^L*9zP8cCuJ>@o zZdaR*yL6);xT*BRqYDh9Uy&+k?G%?@sb$0aws(fYL7HG+U@_fQMVSB@?Sv$4SJ1&z{}Gl*)Siuw^OcgI-0jbw{9pO`4qVK$-}JU>_KuQx&O-r2*jM8ikV27 zSgQQg@G>*`2-}Xx_7`5C0LF|`**X)$;{YD7h&5+dYxj2t-H}ylH_%_YnQLTpZ#7uaQpM&m^{SUL*EAM5JZ8_zZ4_<=Oa_9!wnSecHWz( zQpJmxo%8+eQ2NBasC(T{+*La1sy4^_FMC(q*YZhlS2AU26iPf+p;V&rUeL-`+b zAqEvP(a*1#0-ONR9(+gnz>`~L0Tk@B3+bT^xW`>;FZvgYgUd>{e1+i)lJ=DXI8{#j%vLt} zN;#G8DBhy}vz4xGVHN>Vy&HJ5UygsELVaMgK)ySun%$F*Dfk{-ZB49E!*gVhp^THz z2w7%U6GLnwhy0dF5ZFPP$JMf5j8*2GL#SPCV;#`ez0MOZ^^+5Mw(_iT9a7)g~}iLp>8gG6vDQP6rWJbqf04tAKtL7 zPki7HU6_p{*73*KzydTq0*dUG))p@v-{W$r=!$4ZIh@f|%BpiS#)bB1JQOxk*@L}+ z)nMHbtX&y6^w(P9C>vCiD*zq3wC6z7z=$@-b=lBBwf)B^gIzfQA{qIizE5%Djal5F zU611ziTn1Cj}Wf|fj0|?VyWlZY$MNk+zI5`yhycyr;oKZy1xX#)#lJGB=KxUtB+%P zmn@|PQGv=|Hpp!_=_@SOBC!cF5|~%B5A8uL%5HfQ{k0lE;FbZ+!pc0&0-gcei$%IO zB$l={XlJR1Yt}P9WL($YmoSnXvzt9^jYbkh!(!p;hzAQSu85`A*D-+$R1J$tLB<=( z?F9C+&0>{5kdU{leg1lFUR4qY8qy9fjy`BtWsKex*Aw%+?ajmW>f5mtpEPJEXwh~yEOAC3~rx< znkDtmR7wC+&4VBb6wl8SwPKqHApr$xHZwK^u@>q*!OND)b6K@W7oU)J2~5xR)Eh%6 zcju#W}0PR;muXaqpHVFJVy`qAUkXk}PxH?=A~q`;g;`iJUau<$*JTv|9kZy8+@+LA|GC2nN_WXOz8d zFLY?BU~u&0FRCkjuF{!-spQyw?9RD!j_N@L*m*1BH>>%*Lk&sle$QD8-7z3sJqHrF zn0l6asZEy7+?}~H^dfQ*QCm+nH(>k5S@D~k+eJQV67X(6*?aq$(#9vrn6ue=0k+)cc;~PU2ueq>7OJC1U42d8^~2ZEh`cB2uQsn@ zMOHv)@(wv^jxhPz(6g*U5)yAL&vhwB20yLbA~Ck9menxDbW?Zlt8;;5?G2WV0fBHfh(0 zt0Vu`RNn#0TIWm5c@!G@o;Nlb15s|&MqPLcd_W$eqs5U4t2so8k%?Q^1lB_CvVIW!U*J-nNBQ0kyw2c<4 zCx>`1t&&Oxj|O2ku>_Wqy$^)g_d(CMcw;jRkj)$=;pJDp4QQ5jmW+O$q^skj$&(zMXRyNv}2W z7}^tqo^?M@iI3(zR|QDQJ5AV&>#|Qd!#T;|0%_@$tFd51maT-~2y<=oCd zN`RKY)wCQa@2cl|b`MBe@{NkTC@&lV5rKBGoBL>%p!YJ$az!#O z+w<25x|x+T-Tx8uu6q3V&{xbjYW_*HDXgdSmtmgX6;!bmhCb^_yyVv+JW{vqr)DZl z=IKk^;igHB^3;7g)#azWFd4%>5&B8w`2pbU|7RYs!&R#rmAHaN?7=lQIiXd=tsVl<-6iL<2HPFcdKmgmpG6-WgCfZ{9gwye^Y?&w*x;_$7<>lqM(BV zbu9Rv{v8RQt%HPfVD@F3VamU*!E`uQZk*LR8n^yE1|Ru3I5A}M*)V0+Rj5uP7F|-z z_rIGTBt5>OqPR9$HI9a(AAf`|2M#bYL&FY*r1f489F9eQYkPQ|JayR{t)}G8Q7<)D zi%a)?_jrt>RPAr3{n*SMdDGiNn|cIU+BC&q)XEFiqlt&6{2e%wy523vxJ7>AbS9!A z%qRIuE6XXD$I^Jg5np%Fk9W?T>|LR6$i&||dcMRUKhj|F)Dfw*I$|hHJ!YSj>SZ^j6k@& z>xo!4ZGa!@tmLBc7)uQ%4KbBej(g9)_zZ2OL_gG3#Q9H~R|1c6kb?lt0l&PwL zmC5wczmjs*FqUSAOJXQ!xTc@zlK9)iQ6GPy?b)cWrP+p`n=4BbzCAsFz#_ID;)$V?qhRb`Jv63R!of6+&lAxKs+NyYQ&7^rB`77i^S#}7> zjeB|qI4W#DcV$|mAeVau0oZ!3IX-OCmlyo0DeOL1xIv2q$i7I^B-@|=oOz`Uk0BdX z&hlFaSWn40NWfJB^!~x*M!(zdrZ1Fsn+{HP8*%yeXB}QSO+o#8;1RxcsK@`OIXpD8<_G-(mXR_q zB8W9Lg~ZsSV{(0uZ{0ec_44bVXM8QVeN@1I34ldt)~}vTJl`AG=RdSzauhi8`!&kR z3*5hdd&{ztR-<@*HG!v#7se@ae#mlypAW2$ow4VH1k7*8`$2kQXCP5csJVATTJ$PX z?_Z=JH_@ut2cB>S zd-7WnV@)y^?(9YH@~EDAXv52wO55SFV(31(2Q?xa>GQIbpqmHV3@K}47f{V70leB? zy%*qyzhyxt;~-x52)9OoJM~iK0>3*m4T}K$4&J7Di=~OKHGW%5n2);T;0FKYLTo=d zzeW(#vKTbS4OL|tRa{Cpa#if!J)+U=J+Z#2P zMQDCk-z}Ux25(eOJs3hVhp*yqxs)@UAc5gJ3aCmr30O;k(_RZ@z#(qGS}-zrLB_sZ z>=bV%&*JOWhpW-X&q#{+gI0bMhb47d&Xs&qpdF*iwClA~g7?x;&SHa(*Rxpm1OL6W z`ZH9^e=2$HFvBGLrWFI% zVvk!gJggBWuR)^ep3S;)7irnZbA) z_bKa|MWP655fj?bemtO3>1jF#U>6Hglo-53bwn6K_`_$dNiX;aRaEz81vCmW@~-jW zLdDB$Us%tN2+%IIVk(^-M9;yamCoUWM|TfoBlD4xcZP6=j4`vl&;Yo^U%!k}M;fu@ zT5BS!B?Gs8vFLfwQ^&LDhVz;ZW{Wt6;lE`xS^7+> zr7@`i+2M+sxk2*cJjgkz7 z!RT4*1bx_L>(U$7UAG8HTjgs$=r}r@1Tg8)l05-!OFk_!e zG@Wl~wq3>-%;nizE$)90$fLoS7(}z6expA18h$mWGcKbAmsW6Y`0^*IAjGTGl67z1 zD3Ov)^$@s5P&u&KB!RRbT)fOj#eQMMz10Z4acTxoosgSaDt()yFkfJX*Z!K!DQ^}d zltFRNWW_#aMv2|=8j^LsT@qSZr@yeQ18QE#s6gjcsq?zXYy>>N|yyyz@k%X>J!_^<7z zgzbzWPZXE+U!|N4M_CH@m5h=TmkU;bZ@=*7>%EFMwfja7Ze)C0(6|xJdy_0ZuaEPS zD%<2VLzsxwM>B#b7sB(Vzg_h(rtwg{0ncF$aCW14pO*s10z}2f;4){~H=mvdsGxW2 zmq?r7^ebD>F!g{M*Fr>KfFCEiQ7S%?#P{e3v8_|LPsn7nq!oo-?*l;T^md)a2| z+w&EcVKp&QnD3?XmrdABhkXC;4D|VuHssfMw3o|!1t(v z6Q};^nxTzwgX|8a{+g{z4?Jt^urZ!K0Y@|3I2W*}OI25vK9l}aotD|ptJzK z7C!)v%Bm&9Wj*KvKaZxP&o?O~Yf*y(50*aAoIm=gB0}=kaeD#i6|revp7s5@VP>2M zs##RfBD7tXxJ~9*P|9M-M=!d}dKVoAWBMH+@C>WL9`8b5aeoLc&zfVfN*3*)y_|4^ zlM{_LwnY)lhg6w~M6S}1>3hY=bba4k9-AL$#i|Qw3aqgjdVOrvChR zBm6HbnI7am?oo=hw>+T#c9)jcY${%i0Aa2Bgr5A6B-o8-!X=3NUBl&Kf&(CS0ynM^ z^&qv3QVX-H9G`oo82H^7xdVm26VymLo}&Qa@+Ef+RE~|E?ifSMA{dX{kR z#aFDgEyM5eK|&&|2LZXy6JOV74+_J;Ugn>k7b!dxj0QJ!x8SHD!XQ=tvxSNn&8jr) z4QnZ$Eckj+U?l4|sPD}zv!V-u&o$S&JiXg`J*0ikJT#MBup^>*p zZ3Ahgnb(b7{=6e5#=rQDFcI!lt8!v3j7Vcy(GF4E<))a?m8eHmp!1g8$J(~*>U_Mm zczoQx?yA^TpxNsbZRds?;7;7L=4)3M>40m4rwRzM82~!j zX=>I%q|6WpC|)b1ctU&9r}Nir_jXr734jj`s>P`N@J}46V4&djFx*6z{;`*e0fM)m z_}G+Bu@;LY${7EUBgTR>IDFR;hG4;sAoN%0RI=dOTDZg~>oSBNULW z_}DGk(p-JCAP`A3X-zy4U<9u{)+0r?G>33UzGwZ4Id$91<B?#nzUOT`t%wciws8A)(759 zJbvsCjBEA5w*j(*FUv)Y{?8C>`G+NtPy`7u{hw`FlGN+e5IBMZX;$R0&p;(9e?*)$ z#sa}P#_?Mt-_EAKhv5-8+;M5Z z(%n|7hBD#|w8u)zaI$|x@U@HBA=L3U!T!P9m7%o~yuZiI!uQp#Oqz%oJM}H|P%W+= zG&d8nU&r`Zf_U||4Iwh>UU8vZB{Wn2>|UEh&Q6^}=`W*&p#*L2t@^Qbz|eUFjucx9 z!Rxhcw-#Bm`u5@N(z_R}1orTXD&V)fOQ6<$@2gFA-D>%d#qy8LWe^fM7I@o zOT1>CezwqpD6S?MRZ=oTL@)<@y}*VoHDchHVv>W3^pWuyUcg1PD(zg7`x6qwO?Xrb zPT({fZ#=dOx1>PBN)9v$cc_p#!)rfAGJ+pBi8oQ7CB0y1mBA^j(A=mB31Z$B3%x0&qE+5zv=wU7{nbVq*2F(_O`!) ztfwnLy{ri_?V`fCD{M=RG4LjSG}Xe5Od)pAM#A{+uf2pGyOL`N;IUwmj(38L35yHL zexQsfg^=uU;@#nS>9xLUUOF>y^%(|KYe^9_$xlB@=}VvUvq?28`u@Lp0RFoX=?p^h zbxLQ(@II$lQlwR!zgE)L?1_aZilptH6)A*OL?QlY9^Qi02)fz$06uSpe}i zG28hc%E5j6A0{Y^lcV0DIK0N9aviAbR9kAmKma4#=KR{^-8<3DrQdv`%u)dT$j8f=2+&-b3!Cxl%bw6M`IWRB7UM zX+e0)^sa=NAga=rSTPZrVJ49a?&A#i`pTvVQ?tZ^6wGvU@b^@h9Zxb;8a)CdUhZE!i%vh->)no~PW%33mP3~!l zK`9H}t$YV_WN#b2_uaF50?(GS8A}qBJqzDPE-tH|MIT|b32bHGcE*847B`5J_^5wD zp`2gZghwDFl)H9|?h>dy>bCLnQ76Y=x$%oEn{L$fNN&U%BekV~|6=}BtH=xq7#>1> zk0WVr9AN=6tE+<;;rMnvBH&1rkU>JSCIqovXMPtz`FGCMib`bOgfJhU9S~$C7t{xp zQQvwY0q0?sP<0Zh5>$xqsd=Cc4W(NZXJxyfL#m5hT~9b2N$c?u6VVoZwK!FaD;rlA|A zQril@$nVS6CZTO)935)F{mlCw8z->5q+tK4FDdEw`Sh;dtVCyA&=nVLa{Qt~sHlnP zYVHyuG0~g(aKO6EJ)5K{mXR;GX9&^NqJIrm71oONoOfeVY7wHEEaEg!m;QgW%1U#- zLr&M7V*6@A-Z27L!Y|1C77{Exe%;73F_YKi<<0(0?Wo8T0iaM*upP-~(oc~r3#&0J zMj3}1tUk|oa3E{i^TtrmZI7CP?BLH3@o-6-Dd6JF&2Tzc0>IE>s1 zZGDb-VOcp3_|{m23CL*p9%k9W4kJH_u`Z6zdYfGUUCz|2z)t}5wAA-|-I(vio}*A9 zre3Afp>%9Q5!9IwTPU;MUi{`i%z=B*NJLEDxKFF^S#&6sCig}oo)Vird~Hk2-3MHo zfeq>XIC&5V{4#Or-*3n>|E~qOE>OItg`%|X%mSkIsJ0u7sT^+mvYYg_4;+hQNZ40n z9s}09%u>NTD43P!!kW4alk8Ot!exz2G{szd7SToWto+o*!2s2kfK8^9;yXi|RtUpt zpvTz_X2EqK69UQrGN?n;ZKA%yCGdNcMYX=-S%Fw0$39b%(F z6<~v)lK;NdWm7;3V568GR{CLwsKYBW(m)I(;{$dz!%3J0ROr3sZtw(lsT#iF@%PSg ztwx~69T;I2ali}_*hJGD@LC_HuHln&(D}$kO#Yprdag%Yd^6I3XA%H_^tWmAd{hx3 zmQ$>Mwt@F=i}3D15|163m6x#wg^fypMzjo6%XiP59VrauNjhCHmHkG=wT@s+-Xr3Y zCIu>l0T9_-e}JjJ`%0r}V#E?s^5&#a)@xhUD5<9L-}jqVaAy8Jz$SeH_LPePiV-xa z-Ea=EkHme|2%81V?JoI7)3x%Jgi~F?mL9~oIT+`HqTeW9Wvl_F9>Q5}f7N%Q__y(H zvWKSmJS~rr8M*+lNg-x2qXuyPVn9X?QEEgLS5d~IZm29BU(g^xIe%hPBo@S2ZIy=A zWflNyrwFl_<;Ci+CKApnLZ~oRQKm@|YjDc|f9ED_Me8I?9M`)mD88GsO;6lj)zEnS zGKO!41+2~H$~H}Xv2*&w2zM!=BF46@qLJK4@(9JVaw6p>E+Vr&#jDF00oJg)oG$5J z<3k|CpE#J`YKQ|%24Awc56GWxlQ(Qk8 z;(#Nnyggl?wO6Gu50r);$S$T#NrkCPJtZ+rBT;|ZH!l2&KJ>LCR_k3mXRju}nWs|P z?%e?uQ$D691xuvFhJDl3s-ZH5aa(?%T9>J;yioDC;Tx zA?d~E!`B7L=uO9b|5(xD<+y97j{s7zj3bXBz-8rB*l?8}81DbhwV@RH${M9@R~2#b z_rK#zw_6aHScrJge|VJnDF8mjXQzRCzSR&10r`@V`8mux&XXJai}9~)q-fj~=?w^R zg}ds^FC8SNdviqt)NnDz38_JK)AR9RQr)ik|d?(5IBl21f5qjOSN&}92m?flq5+?PaT*D zD#eHR;%C1BUIR;+M3N*)X=(s7z+>l$t^yEF2krpt>F1IpNhw+eE~Cl+xUNqCk!Gn@ z0Oo*AppafRNs^SX2CxdO#|Qu7dKLg^m4Inr7Z^%Bo+L?1+7mDfoc4P7!#4#0pjoO7 z03*N#P);nKBuPrz4KM{%Kil??`?df?!rg#LU=0}iQ#?tMl(c~ w%Ewe;85sWUJtRp|V(P#=aQ4-fza&W#?;$10?|Xl}*#H0l07*qoM6N<$f~N$djQ{`u literal 0 HcmV?d00001 diff --git a/examples/icons/scatter-plot-64.ico b/examples/icons/scatter-plot-64.ico new file mode 100644 index 0000000000000000000000000000000000000000..4cacb95f4694a4dcc5bc72caf42bd177de859684 GIT binary patch literal 16958 zcmeI1UuaZE6vj7+xDvz_DN;&Ff|MdHh)4+{r3oS;QbeQ#5g$Z~4^l)*DR~fGXsJ*n zh=`O@BuFVDeJDkWNRiNrh!3qu5fQOVDpEp`MI>xx$?fmkyOT~PGrOa^y6eW3FK6b= zIp>~l{@-gR)5X8x;S7HVGOIUcGAlEg%xZutlLyj=es|tZR)BNh?gGPk_$~)$L9#&c z4ZMF_pxia)w--47;{pCt=nej+d_RDD=qGES)}EriuZ5em`)~uj3(a90*Qgs@p)C9Y zE`f{SM=%C@fjj@p=$3`{%TCk3dHvJB-C!kn9V}|6J%oIo>HbIj zJwW@snPWOn@?dd0^aFW3GycT>92nlN)cv#Nb0NULx6!d7?jX=!;_|lZ`x<>tqyFS~ z-O2GW^=9xt<;1mloAT90^KaCj9Cep6`AF8~kIf3O0jveyb&OtjEjzBSTx>M|M*Yb# znm@MUJq}91@X>yD0Cd~AQ2)WQF}ALpw0&ApCdVlL*ewO_UTO0=X6KLXeqguz%BO9g zR+PyxmVd3iT->zRtv4JfFY&~-```iX1IE5p(vzci{bP3r*!la)V>S=$`@m(7Ujf~0 zU)es-yG$Ow@n2%+fz4tt4Sbw!-C5gDWqfq^+YG({$G|Qyd%jR!1awwz1M9$?eZ-B| z>^jf$@t?D&D<`0R&gSYXpL~-1h_?f1F4(bdQQ!C^aT>AZS#|j5ht3zVqmYmP|rZ6g#BHXD{&ab#*)382GIK_2$k_*Dq8jmgbNBT%CV&_P>zt)9Q&IJO1;|NPItX z^0jj}qL ze0@4>o5r75;xY|t<)*eRzFcB;_^$`i7|j^Y136=Bw}B?h1PE7^43G_&D3T5;k5w=-&jIBeuQD zT0g<}Y1*2~-x`bei2W7NTGpLq4+!2#Xp1@5#b=FqhLPx&0pEL>=?mS#RvCL+qJJj* z?PtW4$s^`oFFx~lpc@5-Ye;_W%LBi5;r~tq2kj5tE)R4UB7E<-eyZcw7W}KZd{>9Z z9@)7+d@j}T)10{H`l*iI^{dcMj`)Xf%`qZKpjxZj6gQfBbGazAfW# z@*|&Na17{OHxFulW1(GZ$9LY?eK2C;+Y-Oqz_x2Se>=a}G8_&$JZw(mp*WqwU;N;* z6X*`0^&;p@)O~E;o@x4nwzeOCa$F8Hz7yaY(7M=UIJlBF-3Nm2PPRREF8@b%d#lUj zr+Zxi*z?(xwMPWUHYRucUH%Va{GNvmx#=BP@3w}oPws6bmp13Wy$*Nvht993o&#;p zU-R2H=C)4fp7)(9W?r;8|Kkz-)fX#6Txok3=uS0R#X01lv8U{=|DtJYwIsLQz#eN~ z+51kQ-0{`fr}^RfJx2Fo)yC!jpw+z1^T|>7VexbGx9bOuy)E$>1mbDi+GVw^1#Qb; ze8^AV1E+wUyD94);Qd~XKIZ>P^xn|c{HuNN8hM@oy4&e4@H5a|FSzg1=DydMKC|U@ z>|Fj+_B?t~%aQzbm+Vyj<8ZUb+E>>3+Nu24z^zoH5A>aVz{CIki#q4syx}$i9szgk zrQ0TZq&xoJ??i6w7o`rrH-PR@`u2VqjDb}qf66ZZf@xa_(!<~TowQT?0{%Kv?e%e^ zQ@Q$h!Tn>0|G3YtlXdXl4}JoNL2oB>b-CqJMVG60>Rs6+{YA*nWSLE7y5`DqCaqUx z@laYX^rdp5FSVb@^;GO8a=%w4v4}U(Kcj|hp}*WU)1PFEL*=e=R&C|3l94@Wy(l@; z*He+DT(+coF`HEEkmX!jR~+=EA>ty7{gS0@A@{Fju8=DnAigrS(o8fso z6Ro7c(ySz+Oho_DKudc;r1?Ti@e!s)6g|w!q|C_K#3mIni6ulNL}c0NkNI7@-aGUA z&E0#i_qz6lg}rCa%$(nuGiS~@GXlUW^CWN=@IA0ZXnBUq1dUdi=YXZj@3Fu&z_i+l zbaBxDOfR4fu7x1ALIcpHfYz-Rg47BP9$Mo?m!gL7eQc*vfK^5Uvw)#MJFpjcLuh#z z{1)J&U6QcMJP7;(JPiy3nt^A4U#&8CLnqz>R+)joI{ZGZ8?fFg)6{8J+k-h2JNSHb z2WE9<1q_Se8F5KCL90w3;6>n8+{zi?U0{>Yvb~IU+)UIDT1eofQ1MxUk644TY=-r-)3OA&~gRTp~EXcnhEF7j(?Q_5L)gA zc8A*QX|;X$CW6NRCp$Vpy(?e`a3F){m2!^4y7mD=%QoOX;8=j=RbX{FbNGBf-6#M+ zXnDvgb0hA!yB!zD9|YD2EkF15vC8xS+J%?>bxrn2(pNGV_F%ivwMsBHNlD2Z|I>Zv~N+6n{K8 znHE`P9treoiWqx@ zJmqUhBE~_$1YkI>Ao_D!A8?W2Z@~3=v^L<1jwBdXnJ&Pegey2X3>R8{=jq=Je2U-9 zTY!;5%fCyND-EG7(9x~YhwP6BxYB#)0_W3Mt4w#R%y_HJB&$qwN|`&ye-*F||5j@N7FcB_#L8Ta zKJFu-ZAo2(pjMCSZt%{zJt02U0@r3s7=BATh16xU$n}V0T)vB7i<+ z@NK}Ay?UkeTbj~#0ZQ5eR+*tVXVt%ez8RRF#N;tJ5y@AS9|$c^T4g5UPJ*7EK1+m_ zA4HBHZ|YW={YzgG*KJ3E93~4a9;HC&d%ddbzxGb;*mqTm^#t1E+kCXu@ z%>w4(*LV)2ahczatbv4HfR+)Y{GjA^7J7$%cD74&@nEBokWUeSmIq$PPYdf$p z_rogF6&K zcN;FI&yTWNad~;+@#8Cd6=HPk{MAB9fsoMhBrwe?^DM56b_Qq(gq<-?Glr+C=+Mw|tIS$ll-LFMs~$c)hl_F*o`kkp*V~;-|3S5Ts;k;b|1Zv*5>)YE${Cv?qr%9a)L1zoSgv; xm-`kN-+(JYU9Q;G2+Y9kd*3BQXGRM!=U?ol>O(MSvNZqz002ovPDHLkV1gU8Ex-T( literal 0 HcmV?d00001 diff --git a/examples/packaging/README.md b/examples/packaging/README.md new file mode 100644 index 0000000..724d543 --- /dev/null +++ b/examples/packaging/README.md @@ -0,0 +1,176 @@ +# Packaging Resources + +This directory contains supplemental packaging assets and templates used to produce native installers and portable distributions beyond the default jpackage outputs. + +See `docs/JPACKAGE.md` for detailed `jlink` / `jpackage` guidance and cross-architecture runtime image examples. + +## Files + + | File | Purpose | + | ------ | --------- | + | `graph-digitizer.desktop` | Freedesktop desktop entry used for Linux menu integration and AppImage. | + | `appimage-builder.yml` | Template consumed by `appimage-builder` to generate an AppImage from jpackage app-image output. | + | `deb/postinst`, `deb/prerm` | Maintainer scripts (templates) for Debian packages (install desktop entry & icon caches, cleanup). | + | `rpm/graph-digitizer.spec` | RPM spec template describing metadata, files, scriptlets. | + | `../build/icons/` | Icon assets (copied automatically from repository root `icons/` by Maven during `prepare-package`). | + | `selected-icon.properties` | Optional generated properties overriding `icon.win`, `icon.mac`, `icon.linux`. Fallback file provided. | + +## Desktop Entry (`graph-digitizer.desktop`) + +The desktop file declares: + + +- `Name`, `Comment`: Display metadata + +- `Exec`: Launcher name produced by jpackage (`graph-digitizer`) + +- `Icon`: Logical icon reference; when installed system-wide place icon under `usr/share/icons/hicolor/...` + +- `Categories`: Classification for desktop environment menus + +For DEB/RPM packaging you may optionally install this file as part of a post-install step. For AppImage it is bundled automatically by the build script. + +## AppImage Template (`appimage-builder.yml`) + +The template assumes you have run: + +```bash +mvn -Pnative -Djpackage.type=app-image package + +``` + +This produces `graph-digitizer-java/target/GraphDigitizer` which becomes the ingredient for AppImage creation. The template script section: + + +1. Copies desktop entry into `usr/share/applications` + +2. Installs a 256x256 icon into `usr/share/icons/hicolor/256x256/apps/` + +3. Leaves remaining runtime contents under `usr/bin` + +### Build Example + +```bash +# Install appimage-builder (refer to official docs) +appimage-builder --recipe graph-digitizer-java/packaging/appimage-builder.yml + +``` + +Resulting artifact: `GraphDigitizer-x86_64.AppImage` (optionally create a `.zsync` file for delta updates). + +### Adding Delta Updates (.zsync) + +AppImage can support binary delta updates via a companion `.zsync` file. After building the AppImage: + +```bash +# Extract update information (optional embedded metadata) +export APPIMAGE=GraphDigitizer-x86_64.AppImage + +# Generate .zsync using appimagetool (needs AppImageKit/appimagetool installed) +appimagetool --generate-update-info "$APPIMAGE" > update-info.txt || true +appimagetool --embed-update-information update-info.txt "$APPIMAGE" +appimagetool --create-zsync "$APPIMAGE" + +``` + +Host both the `.AppImage` and `.AppImage.zsync` at a stable URL. Users of AppImage update tools (e.g. `AppImageUpdate`) can then perform efficient incremental updates. + +## Icon Management + +Icons are maintained in repository root `icons/` (multiple sizes .png/.ico). During build: + + +- `maven-resources-plugin` copies them to `graph-digitizer-java/build/icons/` + +- Optional selection script `scripts/select-icon.ps1` chooses best sizes and writes `selected-icon.properties` + +- `properties-maven-plugin` reads that properties file (if present) at `initialize` phase overriding `icon.win`, `icon.mac`, `icon.linux` + +- macOS specific `.icns` is generated in CI or locally with `scripts/create-mac-iconset.sh` + +- Fallback `selected-icon.properties` is auto-created if absent so builds never fail; generate a tailored one via `scripts/select-icon.ps1 -DesiredSize 512` on Windows. + +## CI Workflow Integration + +See `.github/workflows/ci.yml`: + + +- Linux job builds JAR, DEB, RPM, AppImage (.AppImage + optional .zsync generation step can be added). + +- macOS job generates `.icns` prior to DMG packaging. + +- Windows job runs icon selection script then builds EXE. + +Artifacts are uploaded separately (`build-linux`, `build-macos`, `build-windows`). See the workflow file for the AppImage build using `appimage-builder`. + +### Adding Automatic Signing (Optional) + +Integrate signing by inserting steps that invoke: + +```pwsh +pwsh scripts/sign-windows.ps1 -PfxPath certs/code_signing.pfx -PasswordEnvVar WINDOWS_CERT_PASS -Files (Get-ChildItem target -Filter *.exe).FullName + +``` + +```bash +./scripts/sign-macos.sh GraphDigitizer.app "Developer ID Application: Your Company" "teamid123" GraphDigitizer.dmg + +``` + +Provide secrets via CI (e.g. GitHub Actions encrypted secrets) and guard steps with `if: startsWith(github.ref, 'refs/tags/')` for releases. + +### Notarization & Stapling (macOS) + +`sign-macos.sh` handles codesign, notarization submission, polling, and stapling. Ensure `xcrun altool` / `notarytool` authentication is configured via keychain or environment variables. + +## Extending Packaging + + +- Add post-install scripts for DEB/RPM: supply maintainer scripts to install `graph-digitizer.desktop`, run `update-desktop-database` and refresh icon caches (see provided `deb/postinst`, `deb/prerm`). + +- Add AppImage update metadata: edit `update-information` field in template. + +- Add additional icon sizes: place new PNGs/ICOs into root `icons/` naming pattern `scatter-plot-SIZE.png`. + +- Add Windows / macOS code signing: use provided scripts to sign executables / DMG. + +- Add `.zsync` generation and publish both `.AppImage` and `.AppImage.zsync`. + +## Verification Checklist + + +- [ ] Icons copied to `build/icons` + +- [ ] `selected-icon.properties` loaded (if generated) + +- [ ] `.icns` generated on macOS (or provided manually) + +- [ ] AppImage built and launches `graph-digitizer` binary + +- [ ] Desktop file present in AppImage (check with `--appimage-extract`) + +- [ ] `.zsync` file generated for AppImage (optional) + +- [ ] DEB postinst installs desktop entry & icons + +- [ ] RPM spec includes desktop file & icon paths + +- [ ] Windows EXE signed (optional) + +- [ ] macOS DMG / app signed & notarized (optional) + +## Troubleshooting + + | Issue | Resolution | + | ------- | ------------ | + | AppImage fails to start | Ensure executable `graph-digitizer` exists in jpackage app-image output. | + | Icon missing in DEB/RPM menu | Verify icon installed under `usr/share/icons/hicolor/256x256/apps/graph-digitizer.png` and run `update-icon-caches`. | + | DMG shows generic icon | Confirm `.icns` path passed via `-Dicon.mac` and file contains expected sizes. | + | Properties file not loaded | Ensure `selected-icon.properties` is at module root `graph-digitizer-java/` when Maven runs. | + | AppImage update fails | Verify `.AppImage.zsync` hosted at correct URL referenced by embedded update info. | + | Unsigned binary warnings | Integrate signing scripts in CI and validate certificate chain. | + | RPM install missing icon | Add `%{_datadir}/icons/hicolor/256x256/apps/graph-digitizer.png` to `%files` in spec. | + +## License + +All packaging assets are distributed under the project Apache 2.0 license unless otherwise noted. diff --git a/examples/packaging/appimage-builder.yml b/examples/packaging/appimage-builder.yml new file mode 100644 index 0000000..01a511d --- /dev/null +++ b/examples/packaging/appimage-builder.yml @@ -0,0 +1,37 @@ +# AppImage Builder template for Graph Digitizer +# Documentation: https://appimage-builder.readthedocs.io/ + +version: 1 +app: + id: graph-digitizer + name: Graph Digitizer + icon: build/icons/scatter-plot-256.png + # The executable produced by jpackage app-image (launcher script/binary name) + exec: graph-digitizer + exec_args: $@ + +runtime: + # Use default runtime; set up necessary modules if missing + env: + APPDIR_LOG_LEVEL: info + +ingredients: + script: + - echo "Using jpackage app-image as ingredient" + - ls -al + paths: + # Point to the jpackage app-image output directory after build. + - target/GraphDigitizer + +script: + # Copy desktop file and icon to AppDir structure + - mkdir -p $APPDIR/usr/share/applications + - cp packaging/graph-digitizer.desktop $APPDIR/usr/share/applications/graph-digitizer.desktop + - mkdir -p $APPDIR/usr/share/icons/hicolor/256x256/apps + - cp build/icons/scatter-plot-256.png $APPDIR/usr/share/icons/hicolor/256x256/apps/graph-digitizer.png + - echo "AppDir prepared" + +AppImage: + arch: x86_64 + update-information: "gh-releases-zsync|mrhunsaker|Graph_Digitizer_Java_Implementation|latest|GraphDigitizer*.AppImage.zsync" + sign-key: null diff --git a/examples/packaging/deb/postinst b/examples/packaging/deb/postinst new file mode 100644 index 0000000..ab3323b --- /dev/null +++ b/examples/packaging/deb/postinst @@ -0,0 +1,32 @@ +#!/bin/sh +# postinst script for Graph Digitizer (Debian/Ubuntu) +# Installed to /var/lib/dpkg/info/.postinst by custom packaging flow (not jpackage default). +# Responsibilities: +# 1. Install desktop file if missing +# 2. Update icon cache +# 3. Print message +set -e +PKGNAME="graph-digitizer" +DESKTOP="/usr/share/applications/graph-digitizer.desktop" +ICON_SRC="/opt/graph-digitizer/lib/runtime/icons/scatter-plot-256.png" +ICON_DEST="/usr/share/icons/hicolor/256x256/apps/graph-digitizer.png" + +case "$1" in + configure) + if [ -f "$ICON_SRC" ] && [ ! -f "$ICON_DEST" ]; then + install -D -m 0644 "$ICON_SRC" "$ICON_DEST" || true + fi + if [ -f "/opt/graph-digitizer/graph-digitizer.desktop" ]; then + install -D -m 0644 "/opt/graph-digitizer/graph-digitizer.desktop" "$DESKTOP" || true + fi + if command -v update-icon-caches >/dev/null 2>&1; then + update-icon-caches /usr/share/icons/hicolor || true + fi + echo "Graph Digitizer installed. Launch via system menu or 'graph-digitizer'." + ;; + abort-upgrade|abort-remove|abort-deconfigure) + ;; + *) + ;; +fi +exit 0 diff --git a/examples/packaging/deb/prerm b/examples/packaging/deb/prerm new file mode 100644 index 0000000..0ea55ff --- /dev/null +++ b/examples/packaging/deb/prerm @@ -0,0 +1,11 @@ +#!/bin/sh +# prerm script for Graph Digitizer (Debian/Ubuntu) +set -e +case "$1" in + remove|upgrade|deconfigure) + # No special removal logic; desktop/icon cleaned by dpkg automatically if installed via postinst + ;; + *) + ;; +fi +exit 0 diff --git a/examples/packaging/graph-digitizer.desktop b/examples/packaging/graph-digitizer.desktop new file mode 100644 index 0000000..03a50d3 --- /dev/null +++ b/examples/packaging/graph-digitizer.desktop @@ -0,0 +1,11 @@ +[Desktop Entry] +Version=1.0 +Type=Application +Name=Graph Digitizer +Comment=Extract numeric data points from plotted graphs +Exec=graph-digitizer %f +Icon=graph-digitizer +Terminal=false +Categories=Science;Education;Graphics; +StartupWMClass=GraphDigitizer +MimeType=application/x-graphdigitizer; diff --git a/examples/packaging/rpm/graph-digitizer.spec b/examples/packaging/rpm/graph-digitizer.spec new file mode 100644 index 0000000..5efdba3 --- /dev/null +++ b/examples/packaging/rpm/graph-digitizer.spec @@ -0,0 +1,49 @@ +Name: graph-digitizer +Version: 1.2.0 +Release: 1%{?dist} +Summary: Extract numeric data from plotted graphs (JavaFX) +License: Apache-2.0 +URL: https://github.com/mrhunsaker/Graph_Digitizer_Java_Implementation +Source0: %{name}-%{version}.tar.gz + +BuildRequires: java-21-openjdk-devel +Requires: java-21-openjdk + +%description +Graph Digitizer is a JavaFX desktop application that allows users to load +raster images of graphs and extract numeric data points. + +%prep +%setup -q + +%build +# Build via Maven producing jpackage app-image (expects source layout) +mvn -B -Pnative -Djpackage.type=app-image package + +%install +# Install under /opt/graph-digitizer +mkdir -p %{buildroot}/opt/graph-digitizer +cp -r graph-digitizer-java/target/GraphDigitizer/* %{buildroot}/opt/graph-digitizer/ +# Desktop file +install -D -m 0644 graph-digitizer-java/packaging/graph-digitizer.desktop %{buildroot}/usr/share/applications/graph-digitizer.desktop +# Icon (256px) +install -D -m 0644 graph-digitizer-java/build/icons/scatter-plot-256.png %{buildroot}/usr/share/icons/hicolor/256x256/apps/graph-digitizer.png + +%post +if [ -x /usr/bin/update-desktop-database ]; then /usr/bin/update-desktop-database -q || true; fi +if [ -x /usr/bin/gtk-update-icon-cache ]; then /usr/bin/gtk-update-icon-cache -q /usr/share/icons/hicolor || true; fi + +%postun +if [ $1 -eq 0 ]; then + if [ -x /usr/bin/update-desktop-database ]; then /usr/bin/update-desktop-database -q || true; fi + if [ -x /usr/bin/gtk-update-icon-cache ]; then /usr/bin/gtk-update-icon-cache -q /usr/share/icons/hicolor || true; fi +fi + +%files +/opt/graph-digitizer +/usr/share/applications/graph-digitizer.desktop +/usr/share/icons/hicolor/256x256/apps/graph-digitizer.png + +%changelog +* Tue Nov 18 2025 Maintainer - 1.2.0-1 +- Initial RPM spec template diff --git a/examples/pom.xml b/examples/pom.xml new file mode 100644 index 0000000..056dfdf --- /dev/null +++ b/examples/pom.xml @@ -0,0 +1,517 @@ + + + 4.0.0 + + com.digitizer + graph-digitizer + 1.1.1 + jar + + Graph Digitizer + An interactive GUI tool for extracting numeric data points from raster images of graphs + https://github.com/your-repo/graph-digitizer + + + + Michael Ryan Hunsaker + ryhunsaker@dsdmail.net + + developer + + + + + + + Apache License, Version 2.0 + https://www.apache.org/licenses/LICENSE-2.0 + repo + + + + + UTF-8 + 21 + 21 + 21.0.2 + 0.0.8 + 2.23.1 + + + + + + org.openjfx + javafx-controls + ${javafx.version} + + + org.openjfx + javafx-fxml + ${javafx.version} + + + org.openjfx + javafx-swing + ${javafx.version} + + + + + com.google.code.gson + gson + 2.10.1 + + + + + org.apache.commons + commons-csv + 1.10.0 + + + + + com.twelvemonkeys.imageio + imageio-core + 3.10.1 + + + com.twelvemonkeys.imageio + imageio-tiff + 3.10.1 + + + com.twelvemonkeys.imageio + imageio-webp + 3.10.1 + + + com.twelvemonkeys.imageio + imageio-metadata + 3.10.1 + + + + + com.formdev + flatlaf + 3.4.1 + + + com.formdev + flatlaf-intellij-themes + 3.4.1 + + + + + org.slf4j + slf4j-api + 2.0.9 + + + + org.apache.logging.log4j + log4j-api + ${log4j2.version} + + + org.apache.logging.log4j + log4j-core + ${log4j2.version} + + + org.apache.logging.log4j + log4j-slf4j2-impl + ${log4j2.version} + + + + com.lmax + disruptor + 3.4.4 + + + + + com.fasterxml.jackson.core + jackson-databind + 2.15.2 + + + + + junit + junit + 4.13.2 + test + + + org.junit.jupiter + junit-jupiter-api + 5.9.3 + test + + + org.junit.jupiter + junit-jupiter-engine + 5.9.3 + test + + + org.junit.jupiter + junit-jupiter-params + 5.9.3 + test + + + + + + + + org.codehaus.mojo + properties-maven-plugin + 1.1.0 + + + load-selected-icon-properties + initialize + + read-project-properties + + + + ${project.basedir}/selected-icon.properties + + + + + + + + org.apache.maven.plugins + maven-resources-plugin + 3.3.1 + + + copy-icons + prepare-package + + copy-resources + + + ${project.basedir}/build/icons + + + ${project.basedir}/../icons + + **/*.png + **/*.ico + + + + + + + + + org.apache.maven.plugins + maven-compiler-plugin + 3.11.0 + + 21 + 21 + 21 + + + + + + org.openjfx + javafx-maven-plugin + ${javafx.maven.plugin.version} + + com.digitizer.ui.GraphDigitizerApp + + + + + + + org.apache.maven.plugins + maven-shade-plugin + 3.5.0 + + + package + + shade + + + + + com.digitizer.ui.GraphDigitizerApp + + + graph-digitizer + + + + + + + + org.apache.maven.plugins + maven-surefire-plugin + 3.0.0 + + + + + org.apache.maven.plugins + maven-jar-plugin + 3.3.0 + + + + com.digitizer.ui.GraphDigitizerApp + + + + + + + org.apache.maven.plugins + maven-javadoc-plugin + 3.5.0 + + none + false + + + + + + + + + + native + + false + + + + EXE + + ${project.basedir}/build/icons/scatter-plot-256.ico + ${project.basedir}/build/icons/scatter-plot-256.png + ${project.basedir}/build/icons/scatter-plot-256.png + + + + + + + org.apache.maven.plugins + maven-dependency-plugin + 3.5.0 + + + + copy-javafx-windows + prepare-package + + copy + + + + + org.openjfx + javafx-base + ${javafx.version} + win + jar + ${project.build.directory}/jpackage-input/lib + + + org.openjfx + javafx-graphics + ${javafx.version} + win + jar + ${project.build.directory}/jpackage-input/lib + + + org.openjfx + javafx-controls + ${javafx.version} + win + jar + ${project.build.directory}/jpackage-input/lib + + + org.openjfx + javafx-fxml + ${javafx.version} + win + jar + ${project.build.directory}/jpackage-input/lib + + + org.openjfx + javafx-swing + ${javafx.version} + win + jar + ${project.build.directory}/jpackage-input/lib + + + + + + copy-runtime-dependencies + prepare-package + + copy-dependencies + + + ${project.build.directory}/jpackage-input/lib + runtime + org.openjfx + javafx-base,javafx-graphics,javafx-controls,javafx-fxml,javafx-swing + + + + + + + org.apache.maven.plugins + maven-antrun-plugin + 3.1.0 + + + copy-shaded-file-to-jpackage-input + verify + + run + + + + + + + + + + + + + + + + + + + + + + + + org.codehaus.mojo + exec-maven-plugin + 3.1.0 + + + cleanup-app-image + package + + exec + + + ${env.SYSTEMROOT}\System32\WindowsPowerShell\v1.0\powershell.exe + + -NoProfile + -Command + if ('${preserve.appimage}' -ne 'true') { if (Test-Path '${project.build.directory}\jpackage\GraphDigitizer') { Remove-Item -Recurse -Force '${project.build.directory}\jpackage\GraphDigitizer' } } + + + + + jpackage-app-image + verify + + exec + + + ${env.JAVA_HOME}\bin\jpackage.exe + + --type + app-image + --input + ${project.build.directory}/jpackage-input + --main-jar + graph-digitizer.jar + --main-class + com.digitizer.ui.GraphDigitizerApp + --name + GraphDigitizer + --dest + ${project.build.directory}/jpackage + --icon + ${icon.win} + --app-version + ${project.version} + --runtime-image + ${project.build.directory}/jlink-image + --java-options + --module-path $APPDIR\\lib + --java-options + --add-modules=javafx.controls,javafx.fxml,javafx.swing + --java-options + -Djavax.accessibility.assistive_technologies= + + + + + jpackage-msi + verify + + exec + + + ${env.JAVA_HOME}\bin\jpackage.exe + + --type + msi + --app-image + ${project.build.directory}/jpackage/GraphDigitizer + --dest + ${project.build.directory}/jpackage-msi + --name + GraphDigitizer + --icon + ${icon.win} + --app-version + ${project.version} + --win-dir-chooser + --win-menu + --win-shortcut + --win-menu-group + GraphDigitizer + + + + + + + + + + + \ No newline at end of file diff --git a/examples/scripts/README.md b/examples/scripts/README.md new file mode 100644 index 0000000..110e1e6 --- /dev/null +++ b/examples/scripts/README.md @@ -0,0 +1,76 @@ +# Packaging helper scripts + +This folder contains small helper scripts that wrap `jpackage` and related tooling to produce native installers and app-images for different platforms. + +## Overview + + +- All script outputs are written to `target/generated_builds` (previously `scripts/out`). + +- Scripts prefer to dereference symlinks when copying jpackage app-image contents: they use `rsync -aL` when available, and fall back to `cp -rL`. + +## Scripts of interest + + +- `generate-appimage.sh` — produce an `app-image` (and optionally an AppImage if `appimagetool` is installed). + +- `generate-deb.sh` — produce a `.deb` package via `jpackage --type deb`. + +- `generate-rpm.sh` — produce a `.rpm` package via `jpackage --type rpm` or fallback to `app-image` + `fpm`. + +- `generate-dmg.sh` — macOS `.dmg` using `jpackage --type dmg` (run on macOS). + +- `generate-snap.sh` — create a snap from the `app-image` via `snapcraft` (if available). + +- `generate-msi.ps1` — PowerShell script to create Windows MSI installers. + +## Important: Windows MSI multi-arch behavior + + +- `generate-msi.ps1` can produce up to two MSIs in one run: one for `x64` and one for `arm64`. + +- The script requires per-architecture runtime images (produced by `jlink` or downloaded) and accepts them via parameters: + + - `-RuntimeImageX64 ` + + - `-RuntimeImageArm64 ` + +- If a runtime image is not supplied or the path doesn't exist the script will skip that architecture and emit a warning (this is the intended behavior). + +- Example (both arches): + +```powershell +.\scripts\generate-msi.ps1 -AppVersion 1.2.3 -RuntimeImageX64 C:\runtimes\win-x64\jre -RuntimeImageArm64 C:\runtimes\win-arm64\jre + +``` + + +- Example (only x64): + +```powershell +.\scripts\generate-msi.ps1 -AppVersion 1.2.3 -RuntimeImageX64 C:\runtimes\win-x64\jre + +``` + +## Where outputs appear + + +- `target/generated_builds` will contain generated files and folders. MSI files will be renamed to include the architecture, for example: + + - `graph-digitizer-1.2.3-x64.msi` + + - `graph-digitizer-1.2.3-arm64.msi` + +## Creating runtime images + + +- Packaging multi-arch MSIs requires runtime images matching each architecture. See `docs/JPACKAGE.md` for `jlink` and Docker examples for producing `arm64` runtime images on x86_64 hosts. + +## Troubleshooting + + +- If `jpackage` fails with permission or symlink errors, ensure `rsync` is installed (on Unix) or that `cp -rL` is available. The scripts try to avoid creating symlinks in the repo tree. + +- For Windows MSI creation, ensure WiX toolset is installed on the build machine if your Maven flow uses WiX (this script uses `jpackage` directly and expects runtime images). + +If you'd like, open an issue or request in the repository and I can add CI examples to produce runtime images and MSIs per-release. diff --git a/examples/scripts/archive-logs.ps1 b/examples/scripts/archive-logs.ps1 new file mode 100644 index 0000000..3353c0d --- /dev/null +++ b/examples/scripts/archive-logs.ps1 @@ -0,0 +1,45 @@ +<#! +.SYNOPSIS + Archives old Graph Digitizer log files (text & gz) into a timestamped zip. +.DESCRIPTION + Finds log files in .\logs matching graph-digitizer*.log or .log.gz older than -Days threshold. + Moves them into an archive zip (logs/archive-YYYYMMDD-HHMMSS.zip). Skips active current log. +.PARAMETER Days + Minimum age in days for a log file to be archived. Default: 7 +.PARAMETER DryRun + If specified, shows which files would be archived without performing changes. +.EXAMPLE + ./archive-logs.ps1 -Days 14 +.EXAMPLE + ./archive-logs.ps1 -DryRun +#> +param( + [int]$Days = 7, + [switch]$DryRun +) +$logDir = Join-Path -Path (Get-Location) -ChildPath 'logs' +if (!(Test-Path -LiteralPath $logDir)) { + Write-Error "Log directory not found: $logDir"; exit 1 +} +$cutoff = (Get-Date).AddDays(-$Days) +$files = Get-ChildItem -LiteralPath $logDir -File | Where-Object { + ($_).Name -match '^graph-digitizer.*\.log(\.gz)?$' -and ($_.LastWriteTime -lt $cutoff) +} +if (-not $files) { Write-Host "No log files older than $Days days."; exit 0 } +$timestamp = Get-Date -Format 'yyyyMMdd-HHmmss' +$archiveName = "archive-$timestamp.zip" +$archivePath = Join-Path $logDir $archiveName +Write-Host "Preparing archive: $archivePath" +foreach ($f in $files) { + if ($DryRun) { Write-Host "[DRY] Include: $($f.Name)"; continue } +} +if ($DryRun) { Write-Host "Dry run complete."; exit 0 } +Add-Type -AssemblyName System.IO.Compression.FileSystem +$zip = [System.IO.Compression.ZipFile]::Open($archivePath, 'Create') +foreach ($f in $files) { + Write-Host "Archiving: $($f.Name)" + [System.IO.Compression.ZipFileExtensions]::CreateEntryFromFile($zip, $f.FullName, $f.Name) + Remove-Item -LiteralPath $f.FullName -Force +} +$zip.Dispose() +Write-Host "Archive created: $archivePath" diff --git a/examples/scripts/ci/build_runtime_in_container.sh b/examples/scripts/ci/build_runtime_in_container.sh new file mode 100644 index 0000000..c0e0e00 --- /dev/null +++ b/examples/scripts/ci/build_runtime_in_container.sh @@ -0,0 +1,35 @@ +#!/usr/bin/env bash +set -euo pipefail + +# Helper script run inside a container to create a per-arch runtime artifact. +# Usage: ./build_runtime_in_container.sh +# Example: ./build_runtime_in_container.sh amd64 + +ARCH="$1" +OUT_DIR="target/generated_builds" +RT_DIR="$OUT_DIR/runtime-${ARCH}" + +echo "Building runtime for arch: ${ARCH}" +mkdir -p "${RT_DIR}" + +if command -v jlink >/dev/null 2>&1; then + echo "jlink detected. Running example jlink invocation (customize for your project)." + # NOTE: Customize the following jlink command for your project modules and options. + # Example placeholder that will create a minimal runtime image. Replace --add-modules with your app modules. + jlink --add-modules java.base --output "${RT_DIR}/runtime" || true +else + echo "jlink not available in this container. Creating a dummy runtime layout for example purposes." + mkdir -p "${RT_DIR}/runtime/bin" + cat > "${RT_DIR}/runtime/bin/app" <<'EOF' +#!/bin/sh +echo "Hello from runtime ${ARCH}" +EOF + chmod +x "${RT_DIR}/runtime/bin/app" +fi + +echo "Packaging runtime artifact into zip..." +pushd "${RT_DIR}" > /dev/null +zip -r "../runtime-${ARCH}.zip" ./* +popd > /dev/null + +echo "Created ${OUT_DIR}/runtime-${ARCH}.zip" diff --git a/examples/scripts/create-mac-iconset.sh b/examples/scripts/create-mac-iconset.sh new file mode 100644 index 0000000..4d23d80 --- /dev/null +++ b/examples/scripts/create-mac-iconset.sh @@ -0,0 +1,50 @@ +#!/usr/bin/env bash +# Generates a macOS .icns file from the scatter-plot PNG set. +# Run on macOS with iconutil available. +# Usage: ./create-mac-iconset.sh [source-dir] [output.icns] +# Default source-dir: ../build/icons ; default output: scatter-plot.icns +set -euo pipefail +SRC_DIR="${1:-$(dirname "$0")/../build/icons}" +OUT_FILE="${2:-scatter-plot.icns}" +ICONSET_DIR="scatter-plot.iconset" + +if ! command -v iconutil >/dev/null 2>&1; then + echo "iconutil not found; install Xcode command line tools first." >&2 + exit 1 +fi + +rm -rf "$ICONSET_DIR" +mkdir "$ICONSET_DIR" + +# Map sizes to expected file names. +# If you only have one large PNG, resize using sips commands. +copy_or_resize() { + local size=$1 + local retina=$2 + local base_name="scatter-plot-${size}.png" + local src_png="$SRC_DIR/$base_name" + local target_name="icon_${size}x${size}${retina}.png" + if [[ -f "$src_png" ]]; then + cp "$src_png" "$ICONSET_DIR/$target_name" + else + echo "Missing $src_png; attempting resize from largest available." >&2 + local largest=$(ls "$SRC_DIR"/scatter-plot-*.png | sort -V | tail -1) + sips -z "$size" "$size" "$largest" --out "$ICONSET_DIR/$target_name" >/dev/null + fi +} + +# Standard macOS iconset sizes +for s in 16 32 64 128 256 512; do + copy_or_resize $s "" + if [[ $s -ne 16 ]]; then + rs=$((s*2)) + copy_or_resize $s "@2x" # will resize using same base, naming matches iconutil pattern + mv "$ICONSET_DIR/icon_${s}x${s}@2x.png" "$ICONSET_DIR/icon_${s}x${s}@2x.png" 2>/dev/null || true + fi + if [[ $s -eq 512 ]]; then + copy_or_resize 512 "@2x" # 1024x1024 + fi +done + +iconutil -c icns "$ICONSET_DIR" -o "$OUT_FILE" +echo "Created $OUT_FILE from PNG set." \ No newline at end of file diff --git a/examples/scripts/fix-md-lint.ps1 b/examples/scripts/fix-md-lint.ps1 new file mode 100644 index 0000000..ecf2316 --- /dev/null +++ b/examples/scripts/fix-md-lint.ps1 @@ -0,0 +1,27 @@ +# Fix common Markdown lint issues across the repo +# - Replace incorrect closing fences "```text" with "```" +# - Convert angle-bracketed URLs like into explicit markdown links [https://...](https://...) +# - Make a backup copy of each file with extension .bak before changing + +$mdFiles = Get-ChildItem -Path . -Filter *.md -Recurse -File +Write-Host "Found $($mdFiles.Count) markdown files" + +foreach ($f in $mdFiles) { + $text = Get-Content -Raw -LiteralPath $f.FullName + $orig = $text + + # Replace ```text with ``` + $text = $text -replace "```text","```" + + # Convert angle-bracketed URLs to markdown explicit links: => [https://...](https://...) + $text = [regex]::Replace($text, '<(https?://[^>\s]+)>', '[$1]($1)') + + if ($text -ne $orig) { + $bak = $f.FullName + '.bak' + Copy-Item -LiteralPath $f.FullName -Destination $bak -Force + Set-Content -LiteralPath $f.FullName -Value $text -Encoding UTF8 + Write-Host $f.FullName + } +} + +Write-Host "Done. Backups saved as *.md.bak where files were changed." \ No newline at end of file diff --git a/examples/scripts/fix_md_lint.py b/examples/scripts/fix_md_lint.py new file mode 100644 index 0000000..2e62a6f --- /dev/null +++ b/examples/scripts/fix_md_lint.py @@ -0,0 +1,32 @@ +#!/usr/bin/env python3 +"""Fix common markdown lint issues across repo: +- Replace '```text' with '```' +- Convert angle-bracketed URLs like into markdown links [https://...](https://...) +- Save backups as .bak +""" +import re +from pathlib import Path + +root = Path('.').resolve() +md_files = list(root.rglob('*.md')) +print(f'Found {len(md_files)} markdown files') +patched = [] +url_re = re.compile(r'<(https?://[^>\s]+)>') +for p in md_files: + try: + text = p.read_text(encoding='utf-8') + except Exception: + continue + orig = text + text = text.replace('```text', '```') + text = url_re.sub(r'[\1](\1)', text) + if text != orig: + bak = p.with_suffix(p.suffix + '.bak') + p.write_text(orig, encoding='utf-8') if not bak.exists() else None + # Write backup only if not exists + p.with_suffix(p.suffix + '.bak') + Path(str(p) + '.bak').write_text(orig, encoding='utf-8') + p.write_text(text, encoding='utf-8') + print('Patched:', p) + patched.append(str(p)) +print('Done. Patched files:', len(patched)) diff --git a/examples/scripts/fix_md_spacing.py b/examples/scripts/fix_md_spacing.py new file mode 100644 index 0000000..7a0c51b --- /dev/null +++ b/examples/scripts/fix_md_spacing.py @@ -0,0 +1,40 @@ +#!/usr/bin/env python3 +"""Fix markdown spacing issues: +- Ensure blank line before and after fenced code blocks (```) +- Ensure blank line before list blocks (- , * , or numbered lists) + +Backs up modified files as .bak +""" +import re +from pathlib import Path + +root = Path('.').resolve() +md_files = list(root.rglob('*.md')) +print(f'Found {len(md_files)} markdown files') +patched = [] + +# Patterns +fence_start_re = re.compile(r'([^\n])\n(```)', flags=re.M) +fence_end_re = re.compile(r'```\n([^\n`])', flags=re.M) +# list: -, *, or numbered like '1.' followed by space +list_re = re.compile(r'([^\n])\n(?=(?:\s{0,3}[-\*]\s)|(?:\s{0,3}\d+\.\s))', flags=re.M) + +for p in md_files: + text = p.read_text(encoding='utf-8') + orig = text + # Add blank line before fenced code block if missing + text = fence_start_re.sub(r'\1\n\n\2', text) + # Add blank line after fenced code block if missing + text = fence_end_re.sub(r'```\n\n\1', text) + # Add blank line before lists if missing + text = list_re.sub(r'\1\n\n', text) + + if text != orig: + bak = Path(str(p) + '.bak') + if not bak.exists(): + bak.write_text(orig, encoding='utf-8') + p.write_text(text, encoding='utf-8') + print('Patched spacing:', p) + patched.append(str(p)) + +print('Done. Files patched:', len(patched)) diff --git a/examples/scripts/generate-appimage.sh b/examples/scripts/generate-appimage.sh new file mode 100644 index 0000000..8162a20 --- /dev/null +++ b/examples/scripts/generate-appimage.sh @@ -0,0 +1,69 @@ +#!/usr/bin/env bash +set -euo pipefail + +# Usage: ./generate-appimage.sh [app-version] +# Uses jpackage to create an app-image, then (if available) runs appimagetool to produce an AppImage + +APP_NAME=${APP_NAME:-graph-digitizer} +APP_VERSION=${1:-1.0.0} + +if [ -z "${JAVA_HOME:-}" ]; then + echo "ERROR: JAVA_HOME is not set. Please set JAVA_HOME to your JDK (with jpackage) and re-run." >&2 + exit 1 +fi + +JPACKAGE="$JAVA_HOME/bin/jpackage" +if [ ! -x "$JPACKAGE" ]; then + echo "ERROR: jpackage not found or not executable at $JPACKAGE" >&2 + exit 1 +fi + +SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)" +TARGET_DIR="$SCRIPT_DIR/../target" +DEST_DIR="$SCRIPT_DIR/../target/generated_builds" + +JAR=$(ls -t "$TARGET_DIR"/*.jar 2>/dev/null | grep -vE 'sources|original|tests' || true) +JAR="$(echo "$JAR" | head -n1)" + +if [ -z "$JAR" ]; then + echo "ERROR: No JAR found in $TARGET_DIR. Build first (mvn package)." >&2 + exit 1 +fi + +mkdir -p "$DEST_DIR" +APPIMAGE_BUILD_DIR="$DEST_DIR/appimage" +mkdir -p "$APPIMAGE_BUILD_DIR" + +echo "Generating app-image via jpackage..." +"$JPACKAGE" --type app-image --input "$(dirname "$JAR")" --main-jar "$(basename "$JAR")" --name "$APP_NAME" --app-version "$APP_VERSION" --dest "$APPIMAGE_BUILD_DIR" "${@:2}" + +# jpackage will create a directory for the app-image. Locate it. +APP_IMAGE_DIR=$(find "$APPIMAGE_BUILD_DIR" -maxdepth 1 -type d -name "*${APP_NAME}*" -print -quit || true) +if [ -z "$APP_IMAGE_DIR" ]; then + # fallback: try to detect directory containing AppRun or .desktop + APP_IMAGE_DIR=$(find "$APPIMAGE_BUILD_DIR" -maxdepth 2 -type d -exec sh -c 'ls "{}"/AppRun 2>/dev/null >/dev/null && echo {}' \; -print -quit || true) +fi + +if [ -z "$APP_IMAGE_DIR" ]; then + echo "ERROR: Unable to locate the generated AppImage directory under $APPIMAGE_BUILD_DIR" >&2 + exit 1 +fi + +echo "App image folder: $APP_IMAGE_DIR" + +if command -v appimagetool >/dev/null 2>&1; then + echo "appimagetool found; building .AppImage..." + (cd "$APPIMAGE_BUILD_DIR" && appimagetool "$(basename "$APP_IMAGE_DIR")" ) + echo "AppImage created in $APPIMAGE_BUILD_DIR" +else + # ensure app-image is copied into the generated builds area, dereferencing symlinks + OUT_APPIMAGE_DIR="$APPIMAGE_BUILD_DIR/$(basename "$APP_IMAGE_DIR")" + rm -rf "$OUT_APPIMAGE_DIR" + if command -v rsync >/dev/null 2>&1; then + rsync -aL "$APP_IMAGE_DIR"/ "$OUT_APPIMAGE_DIR"/ + else + cp -rL "$APP_IMAGE_DIR"/* "$OUT_APPIMAGE_DIR"/ || true + fi + echo "appimagetool not found. Generated app-image is available at: $OUT_APPIMAGE_DIR" >&2 + echo "Install appimagetool (https://github.com/AppImage/AppImageKit) to create a single-file .AppImage, or distribute the app-image folder." >&2 +fi diff --git a/examples/scripts/generate-deb.sh b/examples/scripts/generate-deb.sh new file mode 100644 index 0000000..fa1219f --- /dev/null +++ b/examples/scripts/generate-deb.sh @@ -0,0 +1,37 @@ +#!/usr/bin/env bash +set -euo pipefail + +# Usage: ./generate-deb.sh [app-version] +# Checks JAVA_HOME, finds the artifact under ../target, and uses jpackage to create a .deb + +APP_NAME=${APP_NAME:-graph-digitizer} +APP_VERSION=${1:-1.0.0} + +if [ -z "${JAVA_HOME:-}" ]; then + echo "ERROR: JAVA_HOME is not set. Please set JAVA_HOME to your JDK (with jpackage) and re-run." >&2 + exit 1 +fi + +JPACKAGE="$JAVA_HOME/bin/jpackage" +if [ ! -x "$JPACKAGE" ]; then + echo "ERROR: jpackage not found or not executable at $JPACKAGE" >&2 + exit 1 +fi + +SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)" +TARGET_DIR="$SCRIPT_DIR/../target" +DEST_DIR="$SCRIPT_DIR/../target/generated_builds" + +JAR=$(ls -t "$TARGET_DIR"/*.jar 2>/dev/null | grep -vE 'sources|original|tests' || true) +JAR="$(echo "$JAR" | head -n1)" + +if [ -z "$JAR" ]; then + echo "ERROR: No JAR found in $TARGET_DIR. Build first (mvn package)." >&2 + exit 1 +fi + +mkdir -p "$DEST_DIR" + +"$JPACKAGE" --type deb --input "$(dirname "$JAR")" --main-jar "$(basename "$JAR")" --name "$APP_NAME" --app-version "$APP_VERSION" --dest "$DEST_DIR" "${@:2}" + +echo "DEB artifact created in $DEST_DIR" diff --git a/examples/scripts/generate-dmg.sh b/examples/scripts/generate-dmg.sh new file mode 100644 index 0000000..7bcf866 --- /dev/null +++ b/examples/scripts/generate-dmg.sh @@ -0,0 +1,37 @@ +#!/usr/bin/env bash +set -euo pipefail + +# Usage: ./generate-dmg.sh [app-version] +# Uses jpackage to create a macOS .dmg. Run on macOS with a JDK that includes jpackage. + +APP_NAME=${APP_NAME:-graph-digitizer} +APP_VERSION=${1:-1.0.0} + +if [ -z "${JAVA_HOME:-}" ]; then + echo "ERROR: JAVA_HOME is not set. Please set JAVA_HOME to your JDK (with jpackage) and re-run." >&2 + exit 1 +fi + +JPACKAGE="$JAVA_HOME/bin/jpackage" +if [ ! -x "$JPACKAGE" ]; then + echo "ERROR: jpackage not found or not executable at $JPACKAGE" >&2 + exit 1 +fi + +SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)" +TARGET_DIR="$SCRIPT_DIR/../target" +DEST_DIR="$SCRIPT_DIR/../target/generated_builds" + +JAR=$(ls -t "$TARGET_DIR"/*.jar 2>/dev/null | grep -vE 'sources|original|tests' || true) +JAR="$(echo "$JAR" | head -n1)" + +if [ -z "$JAR" ]; then + echo "ERROR: No JAR found in $TARGET_DIR. Build first (mvn package)." >&2 + exit 1 +fi + +mkdir -p "$DEST_DIR" + +"$JPACKAGE" --type dmg --input "$(dirname "$JAR")" --main-jar "$(basename "$JAR")" --name "$APP_NAME" --app-version "$APP_VERSION" --dest "$DEST_DIR" "${@:2}" + +echo "DMG created in $DEST_DIR" diff --git a/examples/scripts/generate-javadoc-index.sh b/examples/scripts/generate-javadoc-index.sh new file mode 100644 index 0000000..96ddf18 --- /dev/null +++ b/examples/scripts/generate-javadoc-index.sh @@ -0,0 +1,280 @@ +#!/usr/bin/env bash +# +# generate-javadoc-index.sh +# +# Purpose: +# Generate a lightweight landing page (index.html) and metadata for versioned Javadoc +# publications suitable for a GitHub Pages (gh-pages) branch layout. +# +# Intended workflow: +# 1. After running: mvn site +# 2. Copy/rsync: target/site/apidocs -> /LATEST_TMP +# 3. Invoke this script with the release version (e.g. 1.1) and root docs directory. +# 4. Script moves LATEST_TMP to a versioned subdirectory (//) if not already +# present, updates versions listing, and writes a top-level index.html that: +# - Lists all published versions +# - Redirects (meta refresh + JS) to the latest version's Javadoc package root +# +# Example: +# scripts/generate-javadoc-index.sh \ +# --root gh-pages-workdir \ +# --version 1.1 \ +# --latest +# +# Resulting structure (root = gh-pages-workdir): +# gh-pages-workdir/ +# index.html (landing/redirect page) +# versions.json (machine-readable version metadata) +# versions.txt (plain text list) +# 1.0/ (previous Javadoc) +# 1.1/ (new Javadoc) +# +# Usage: +# generate-javadoc-index.sh --root --version [--latest] [--project-url URL] +# [--title "Graph Digitizer API"] [--package com.digitizer] +# +# Arguments: +# --root Root directory that becomes the GitHub Pages document root. +# --version Semantic version string for current docs (e.g. 1.2.0, 1.2, v1.2). +# --latest Mark this version as the latest (updates redirect target). +# --project-url Optional project homepage (link displayed on index, defaults to https://github.com/mrhunsaker/Graph_Digitizer_Java_Implementation). +# --title Optional custom title for landing page. +# --package Optional Java base package to deep-link into (e.g. com/digitizer/ui). +# --help Show help. +# +# Environment overrides (optional): +# PROJECT_URL, DOC_TITLE, BASE_PACKAGE +# +# Exit codes: +# 0 success +# 2 usage error +# 3 missing directory or move failure +# +# Notes: +# - Idempotent: re-running with same version will not duplicate entries. +# - Will not delete older versions. +# - Designed to be run inside CI before committing to gh-pages. +# +set -euo pipefail + +# --------------------------- +# Default configuration +# --------------------------- +ROOT_DIR="" +VERSION="" +MARK_LATEST=0 +PROJECT_URL="${PROJECT_URL:-https://github.com/mrhunsaker/Graph_Digitizer_Java_Implementation}" +DOC_TITLE="${DOC_TITLE:-Graph Digitizer API Documentation}" +BASE_PACKAGE="${BASE_PACKAGE:-}" +INDEX_FILE="index.html" +VERSIONS_JSON="versions.json" +VERSIONS_TXT="versions.txt" +SITE_BASE="${SITE_BASE:-https://mrhunsaker.github.io/Graph_Digitizer_Java_Implementation}" + +# --------------------------- +# Helpers +# --------------------------- +print_help() { + sed -n '1,100p' "$0" | grep -E "^# " | sed 's/^# //' + cat <&2 + exit 2 +} + +info() { + echo "[info] $*" +} + +# --------------------------- +# Parse arguments +# --------------------------- +while [[ $# -gt 0 ]]; do + case "$1" in + --root) + ROOT_DIR="${2:-}"; shift 2;; + --version) + VERSION="${2:-}"; shift 2;; + --latest) + MARK_LATEST=1; shift;; + --project-url) + PROJECT_URL="${2:-}"; shift 2;; + --title) + DOC_TITLE="${2:-}"; shift 2;; + --package) + BASE_PACKAGE="${2:-}"; shift 2;; + --help|-h) + print_help; exit 0;; + *) + error "Unknown argument: $1" + ;; + esac +done + +# --------------------------- +# Validation +# --------------------------- +[[ -z "${ROOT_DIR}" ]] && error "--root is required" +[[ -z "${VERSION}" ]] && error "--version is required" + +if [[ ! -d "${ROOT_DIR}" ]]; then + error "Root directory '${ROOT_DIR}' does not exist" +fi + +VERSION_DIR="${ROOT_DIR}/${VERSION}" + +# Expect user to have staged Javadoc in a temp location or already in version dir +# Accept either: +# 1. ROOT_DIR/${VERSION} exists (already placed) +# 2. ROOT_DIR/apidocs-temp exists and needs to be moved +TEMP_SRC="${ROOT_DIR}/apidocs-temp" + +if [[ -d "${VERSION_DIR}" ]]; then + info "Version directory already present: ${VERSION_DIR}" +elif [[ -d "${TEMP_SRC}" ]]; then + info "Moving temp Javadoc '${TEMP_SRC}' to '${VERSION_DIR}'" + mv "${TEMP_SRC}" "${VERSION_DIR}" +else + error "Neither version directory '${VERSION_DIR}' nor temp source '${TEMP_SRC}' exists. Place generated Javadoc first." +fi + +# Basic sanity: confirm there is an index.html inside version directory +if [[ ! -f "${VERSION_DIR}/index.html" ]]; then + error "No index.html found inside '${VERSION_DIR}'. Javadoc generation may have failed." +fi + +# --------------------------- +# Update versions metadata +# --------------------------- +VERSIONS_LIST=() +if [[ -f "${ROOT_DIR}/${VERSIONS_TXT}" ]]; then + mapfile -t VERSIONS_LIST < "${ROOT_DIR}/${VERSIONS_TXT}" +fi + +# Add new version if not present +if ! grep -qx "${VERSION}" "${ROOT_DIR}/${VERSIONS_TXT}" 2>/dev/null; then + info "Appending version '${VERSION}' to ${VERSIONS_TXT}" + echo "${VERSION}" >> "${ROOT_DIR}/${VERSIONS_TXT}" +fi + +# Re-read list (sorted descending using version sort, fallback lexical) +mapfile -t VERSIONS_LIST < "${ROOT_DIR}/${VERSIONS_TXT}" +SORTED_VERSIONS=$(printf "%s\n" "${VERSIONS_LIST[@]}" | sort -rV) + +# Create JSON metadata +{ + echo "{" + echo " \"latest\": \"$(printf "%s\n" "${SORTED_VERSIONS}" | head -n1)\"," + echo " \"versions\": [" + COUNT=0 + TOTAL=$(printf "%s\n" "${SORTED_VERSIONS}" | wc -l | tr -d ' ') + while read -r v; do + COUNT=$((COUNT+1)) + COMMA="," + [[ ${COUNT} -eq ${TOTAL} ]] && COMMA="" + echo " \"${v}\"${COMMA}" + done <<< "${SORTED_VERSIONS}" + echo " ]" + echo "}" +} > "${ROOT_DIR}/${VERSIONS_JSON}" + +info "Wrote versions metadata: ${VERSIONS_JSON}" + +LATEST_VERSION=$(jq -r '.latest' "${ROOT_DIR}/${VERSIONS_JSON}" 2>/dev/null || printf "%s" "$(printf "%s\n" "${SORTED_VERSIONS}" | head -n1)") + +# If explicitly marking latest and differs, force override +if [[ ${MARK_LATEST} -eq 1 && "${LATEST_VERSION}" != "${VERSION}" ]]; then + info "Overriding latest to '${VERSION}' (was '${LATEST_VERSION}')" + LATEST_VERSION="${VERSION}" + # Re-write JSON with explicit override + { + echo "{" + echo " \"latest\": \"${LATEST_VERSION}\"," + echo " \"versions\": [" + COUNT=0 + TOTAL=$(printf "%s\n" "${SORTED_VERSIONS}" | wc -l | tr -d ' ') + while read -r v; do + COUNT=$((COUNT+1)) + COMMA="," + [[ ${COUNT} -eq ${TOTAL} ]] && COMMA="" + echo " \"${v}\"${COMMA}" + done <<< "${SORTED_VERSIONS}" + echo " ]" + echo "}" + } > "${ROOT_DIR}/${VERSIONS_JSON}" +fi + +# --------------------------- +# Landing page (index.html) +# --------------------------- +REDIRECT_TARGET="${SITE_BASE}/${LATEST_VERSION}/index.html" +# Deep package linking disabled to ensure redirect hits version root index.html +# (Previously attempted to link into package path which lacked its own index.html) +PROJECT_LINK_HTML="

Project Homepage

" + +# Generate HTML +cat > "${ROOT_DIR}/${INDEX_FILE}" < + + + + ${DOC_TITLE} + + + + + +

${DOC_TITLE}

+

You will be redirected shortly to the latest API documentation: ${LATEST_VERSION}.

+ ${PROJECT_LINK_HTML} +
+

Published Versions

+
    +EOF + +while read -r v; do + CLASS="" + [[ "${v}" == "${LATEST_VERSION}" ]] && CLASS="latest" + echo "
  • ${v} $( [[ "${CLASS}" == "latest" ]] && echo "(latest)" )
  • " >> "${ROOT_DIR}/${INDEX_FILE}" +done <<< "${SORTED_VERSIONS}" + +cat >> "${ROOT_DIR}/${INDEX_FILE}" < +
+ + + +EOF + +info "Landing page generated: ${ROOT_DIR}/${INDEX_FILE}" +info "Redirect target: ${REDIRECT_TARGET}" +info "Latest version: ${LATEST_VERSION}" +info "Done." diff --git a/examples/scripts/generate-msi.ps1 b/examples/scripts/generate-msi.ps1 new file mode 100644 index 0000000..88a1bbf --- /dev/null +++ b/examples/scripts/generate-msi.ps1 @@ -0,0 +1,86 @@ +Param( + [string]$AppName = "graph-digitizer", + [string]$AppVersion = "1.0.0", + [string]$RuntimeImageX64 = "", + [string]$RuntimeImageArm64 = "" +) + +if (-not $env:JAVA_HOME -or [string]::IsNullOrWhiteSpace($env:JAVA_HOME)) { + Write-Error "JAVA_HOME is not set. Please set JAVA_HOME to your JDK installation before running this script. Example: $env:JAVA_HOME = 'C:\Program Files\Java\jdk-17'" + exit 1 +} + +$jpackage = Join-Path $env:JAVA_HOME "bin\jpackage.exe" +if (-not (Test-Path $jpackage)) { + Write-Error "jpackage not found at $jpackage. Please ensure your JDK includes jpackage and JAVA_HOME points to it." + exit 1 +} + +$scriptDir = Split-Path -Parent $MyInvocation.MyCommand.Definition +$targetDir = Join-Path $scriptDir "..\target" + +$jar = Get-ChildItem -Path $targetDir -Filter "*.jar" -File | Where-Object { $_.Name -notmatch 'sources|original|tests' } | Sort-Object LastWriteTime -Descending | Select-Object -First 1 +if (-not $jar) { + Write-Error "No application JAR found under $targetDir. Build the project first (e.g. mvn package)." + exit 1 +} + +$outDir = Join-Path $scriptDir "..\target\generated_builds" +if (-not (Test-Path $outDir)) { New-Item -ItemType Directory -Path $outDir -Force | Out-Null } + +# Determine runtime images: prefer explicit parameters, fallback to environment variables +if ([string]::IsNullOrWhiteSpace($RuntimeImageX64)) { $RuntimeImageX64 = $env:RUNTIME_IMAGE_X64 } +if ([string]::IsNullOrWhiteSpace($RuntimeImageArm64)) { $RuntimeImageArm64 = $env:RUNTIME_IMAGE_ARM64 } + +$arches = @( + @{ Name = 'x64'; Runtime = $RuntimeImageX64; Suffix = 'x64' }, + @{ Name = 'arm64'; Runtime = $RuntimeImageArm64; Suffix = 'arm64' } +) + +$overallExit = 0 +foreach ($arch in $arches) { + $name = $arch.Name + $runtime = $arch.Runtime + $suffix = $arch.Suffix + + if ([string]::IsNullOrWhiteSpace($runtime)) { + Write-Warning "Skipping $name: no runtime image provided. Set -RuntimeImage$($suffix.ToUpper()) or environment variable RUNTIME_IMAGE_$($suffix.ToUpper())." + $overallExit = 2 + continue + } + + if (-not (Test-Path $runtime)) { + Write-Warning "Runtime image path for $name does not exist: $runtime. Skipping." + $overallExit = 2 + continue + } + + Write-Host "Generating MSI for architecture: $name using runtime image: $runtime" + + # Run jpackage with the provided runtime image + & $jpackage --type msi --input $jar.DirectoryName --main-jar $jar.Name --name $AppName --app-version $AppVersion --dest $outDir --runtime-image $runtime @Args + + if ($LASTEXITCODE -ne 0) { + Write-Error "jpackage failed for $name with exit code $LASTEXITCODE" + $overallExit = $LASTEXITCODE + continue + } + + # Find the newest MSI produced in the output directory and rename it to include arch suffix + $msi = Get-ChildItem -Path $outDir -Filter "*.msi" -File | Sort-Object LastWriteTime -Descending | Select-Object -First 1 + if ($msi) { + $newName = "{0}-{1}-{2}{3}" -f $AppName, $AppVersion, $suffix, $msi.Extension + $newPath = Join-Path $outDir $newName + try { + Move-Item -Path $msi.FullName -Destination $newPath -Force + Write-Host "MSI created: $newPath" + } catch { + Write-Warning "Failed to rename MSI $($msi.FullName) to $newPath: $_" + Write-Host "MSI left at: $($msi.FullName)" + } + } else { + Write-Warning "No MSI found in $outDir after jpackage for $name" + } +} + +exit $overallExit diff --git a/examples/scripts/generate-rpm.sh b/examples/scripts/generate-rpm.sh new file mode 100644 index 0000000..ec1d028 --- /dev/null +++ b/examples/scripts/generate-rpm.sh @@ -0,0 +1,118 @@ +#!/usr/bin/env bash +set -euo pipefail + +# Usage: ./generate-rpm.sh [app-version] +# Checks JAVA_HOME, finds the artifact under ../target, and uses jpackage to create a .rpm + +APP_NAME=${APP_NAME:-graph-digitizer} +APP_VERSION=${1:-1.0.0} + +if [ -z "${JAVA_HOME:-}" ]; then + echo "ERROR: JAVA_HOME is not set. Please set JAVA_HOME to your JDK (with jpackage) and re-run." >&2 + exit 1 +fi + +JPACKAGE="$JAVA_HOME/bin/jpackage" +if [ ! -x "$JPACKAGE" ]; then + echo "ERROR: jpackage not found or not executable at $JPACKAGE" >&2 + exit 1 +fi + +SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)" +TARGET_DIR="$SCRIPT_DIR/../target" +DEST_DIR="$SCRIPT_DIR/../target/generated_builds" + +JAR=$(ls -t "$TARGET_DIR"/*.jar 2>/dev/null | grep -vE 'sources|original|tests' || true) +JAR="$(echo "$JAR" | head -n1)" + +if [ -z "$JAR" ]; then + echo "ERROR: No JAR found in $TARGET_DIR. Build first (mvn package)." >&2 + exit 1 +fi + +mkdir -p "$DEST_DIR" + +echo "Attempting to create RPM with jpackage..." +if "$JPACKAGE" --type rpm --input "$(dirname "$JAR")" --main-jar "$(basename "$JAR")" --name "$APP_NAME" --app-version "$APP_VERSION" --dest "$DEST_DIR" "${@:2}"; then + echo "RPM artifact created in $DEST_DIR" + exit 0 +else + echo "jpackage did not support RPM on this system or failed. Falling back to app-image + fpm (if available)." >&2 +fi + +# Fallback: create an app-image in a temporary system dir (avoid filesystem permission issues), then use fpm to create an rpm if possible +TMP_APPIMAGE_DIR=$(mktemp -d /tmp/${APP_NAME}.appimage.XXXX) +# preserve flag: when set to 1 we will keep the tmp dir for inspection +PRESERVE_TMP=0 +trap 'if [ "$PRESERVE_TMP" -eq 0 ]; then rm -rf "$TMP_APPIMAGE_DIR"; fi' EXIT +echo "Generating app-image via jpackage in temporary directory $TMP_APPIMAGE_DIR..." +"$JPACKAGE" --type app-image --input "$(dirname "$JAR")" --main-jar "$(basename "$JAR")" --name "$APP_NAME" --app-version "$APP_VERSION" --dest "$TMP_APPIMAGE_DIR" "${@:2}" + +FOUND_APP_DIR=$(find "$TMP_APPIMAGE_DIR" -maxdepth 1 -type d -name "*${APP_NAME}*" -print -quit || true) +if [ -z "$FOUND_APP_DIR" ]; then + echo "ERROR: Unable to locate jpackage app-image directory under $TMP_APPIMAGE_DIR" >&2 + exit 1 +fi + +echo "App image produced at: $FOUND_APP_DIR" + +mkdir -p "$DEST_DIR" + +if command -v fpm >/dev/null 2>&1; then + echo "fpm found; creating RPM from app-image contents..." + # Use a tmp copy on the local filesystem (/tmp) to avoid symlink/FS issues on repo mount + TMP_COPY=$(mktemp -d /tmp/${APP_NAME}.rpmroot.XXXX) + mkdir -p "$TMP_COPY/opt/$APP_NAME" + # prefer rsync (dereference symlinks) if available + if command -v rsync >/dev/null 2>&1; then + rsync -aL "$FOUND_APP_DIR"/ "$TMP_COPY/opt/$APP_NAME/" + else + cp -r "$FOUND_APP_DIR"/* "$TMP_COPY/opt/$APP_NAME/" || true + fi + + (cd "$TMP_COPY" && fpm -s dir -t rpm -n "$APP_NAME" -v "$APP_VERSION" --prefix / -C . opt) \ + && mv "$TMP_COPY"/*.rpm "$DEST_DIR/" 2>/dev/null || true + + # clean tmp copy + rm -rf "$TMP_COPY" + + if ls "$DEST_DIR"/*.rpm >/dev/null 2>&1; then + echo "RPM created in $DEST_DIR" + exit 0 + else + echo "fpm failed to produce an RPM. Check fpm output above." >&2 + PRESERVE_TMP=1 + echo "App-image preserved at: $TMP_APPIMAGE_DIR" >&2 + exit 1 + fi +else + echo "fpm not found. To use the fallback, install 'fpm' (https://github.com/jordansissel/fpm) or install system rpm packaging tools so jpackage can produce rpm (e.g. rpm-build)." >&2 + # attempt to copy app-image folder back to generated_builds for inspection, prefer rsync to dereference symlinks + OUT_APPIMAGE_DIR="$DEST_DIR/appimage" + rm -rf "$OUT_APPIMAGE_DIR" + mkdir -p "$(dirname "$OUT_APPIMAGE_DIR")" + if command -v rsync >/dev/null 2>&1; then + if rsync -aL "$FOUND_APP_DIR"/ "$OUT_APPIMAGE_DIR"/; then + echo "App-image copied to: $OUT_APPIMAGE_DIR" >&2 + # clean tmp dir + PRESERVE_TMP=0 + rm -rf "$TMP_APPIMAGE_DIR" + exit 1 + else + echo "rsync failed to copy app-image to $OUT_APPIMAGE_DIR. Preserving temporary app-image at: $TMP_APPIMAGE_DIR" >&2 + PRESERVE_TMP=1 + exit 1 + fi + else + # fallback: try to use cp with -L to dereference symlinks + if cp -rL "$FOUND_APP_DIR"/* "$OUT_APPIMAGE_DIR"/ 2>/dev/null; then + echo "App-image copied to: $OUT_APPIMAGE_DIR (via cp -rL)" >&2 + PRESERVE_TMP=0 + rm -rf "$TMP_APPIMAGE_DIR" + exit 1 + fi + echo "rsync not available and cp -rL failed. Preserving temporary app-image at: $TMP_APPIMAGE_DIR" >&2 + PRESERVE_TMP=1 + exit 1 + fi +fi diff --git a/examples/scripts/generate-snap.sh b/examples/scripts/generate-snap.sh new file mode 100644 index 0000000..785456b --- /dev/null +++ b/examples/scripts/generate-snap.sh @@ -0,0 +1,86 @@ +#!/usr/bin/env bash +set -euo pipefail + +# Usage: ./generate-snap.sh [app-version] +# Attempts to create a snap from the jpackage app-image output using snapcraft if available. + +APP_NAME=${APP_NAME:-graph-digitizer} +APP_VERSION=${1:-1.0.0} + +if [ -z "${JAVA_HOME:-}" ]; then + echo "ERROR: JAVA_HOME is not set. Please set JAVA_HOME to your JDK (with jpackage) and re-run." >&2 + exit 1 +fi + +JPACKAGE="$JAVA_HOME/bin/jpackage" +if [ ! -x "$JPACKAGE" ]; then + echo "ERROR: jpackage not found or not executable at $JPACKAGE" >&2 + exit 1 +fi + +SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)" +TARGET_DIR="$SCRIPT_DIR/../target" +DEST_DIR="$SCRIPT_DIR/../target/generated_builds" + +JAR=$(ls -t "$TARGET_DIR"/*.jar 2>/dev/null | grep -vE 'sources|original|tests' || true) +JAR="$(echo "$JAR" | head -n1)" + +if [ -z "$JAR" ]; then + echo "ERROR: No JAR found in $TARGET_DIR. Build first (mvn package)." >&2 + exit 1 +fi + +mkdir -p "$DEST_DIR" +APPIMAGE_BUILD_DIR="$DEST_DIR/appimage" +mkdir -p "$APPIMAGE_BUILD_DIR" + +echo "Generating app-image via jpackage (snap building uses the app-image)..." +"$JPACKAGE" --type app-image --input "$(dirname "$JAR")" --main-jar "$(basename "$JAR")" --name "$APP_NAME" --app-version "$APP_VERSION" --dest "$APPIMAGE_BUILD_DIR" "${@:2}" + +# Locate AppDir produced by jpackage +APP_IMAGE_DIR=$(find "$APPIMAGE_BUILD_DIR" -maxdepth 1 -type d -name "*${APP_NAME}*" -print -quit || true) +if [ -z "$APP_IMAGE_DIR" ]; then + echo "ERROR: Could not find app-image dir. Aborting." >&2 + exit 1 +fi + +if command -v snapcraft >/dev/null 2>&1; then + echo "snapcraft found; creating temporary snap project..." + TMP_SNAP="$DEST_DIR/snap-temp" + rm -rf "$TMP_SNAP" + mkdir -p "$TMP_SNAP/prime" + # copy appimage contents into prime, dereference symlinks when possible + if command -v rsync >/dev/null 2>&1; then + rsync -aL "$APP_IMAGE_DIR"/ "$TMP_SNAP/prime/" + else + cp -rL "$APP_IMAGE_DIR"/* "$TMP_SNAP/prime/" || true + fi + + cat > "$TMP_SNAP/snapcraft.yaml" </dev/null 2>&1; then + rsync -aL "$APP_IMAGE_DIR"/ "$OUT_APPIMAGE_DIR"/ + else + cp -rL "$APP_IMAGE_DIR"/* "$OUT_APPIMAGE_DIR"/ || true + fi + echo "snapcraft not found. I generated an app-image under: $OUT_APPIMAGE_DIR" >&2 + echo "To build a snap automatically, install snapcraft and re-run this script. Alternatively, package manually using snapcraft pack in a project directory." >&2 +fi diff --git a/examples/scripts/ingest-json-log.ps1 b/examples/scripts/ingest-json-log.ps1 new file mode 100644 index 0000000..9c1947c --- /dev/null +++ b/examples/scripts/ingest-json-log.ps1 @@ -0,0 +1,44 @@ +<#! +.SYNOPSIS + Simple PowerShell ingestion example for the newline-delimited JSON log produced by Graph Digitizer. +.DESCRIPTION + Reads the file logs/graph-digitizer.json (or a supplied -Path) and filters by Level or Logger. + Demonstrates how structured logs can be piped into other tools. +.PARAMETER Path + Path to the JSON log file (default: logs/graph-digitizer.json) +.PARAMETER Level + Optional level filter (e.g. INFO, DEBUG, ERROR) +.PARAMETER Logger + Optional logger name prefix filter (e.g. com.digitizer) +.EXAMPLE + ./ingest-json-log.ps1 -Level ERROR +.EXAMPLE + ./ingest-json-log.ps1 -Logger com.digitizer.core +#> +param( + [string]$Path = "logs/graph-digitizer.json", + [string]$Level, + [string]$Logger +) + +if (!(Test-Path -LiteralPath $Path)) { + Write-Error "Log file not found: $Path"; exit 1 +} + +Get-Content -LiteralPath $Path | ForEach-Object { + if ([string]::IsNullOrWhiteSpace($_)) { return } + try { + $obj = $_ | ConvertFrom-Json -ErrorAction Stop + } catch { + Write-Warning "Skipping malformed line"; return + } + if ($Level -and $obj.level -ne $Level) { return } + if ($Logger -and ($obj.logger -notlike "$Logger*")) { return } + [pscustomobject]@{ + Time = $obj.time + Level = $obj.level + Logger = $obj.logger + Thread = $obj.thread + Msg = $obj.message + } +} | Format-Table -AutoSize diff --git a/examples/scripts/ingest_json_log.py b/examples/scripts/ingest_json_log.py new file mode 100644 index 0000000..8b1c619 --- /dev/null +++ b/examples/scripts/ingest_json_log.py @@ -0,0 +1,49 @@ +"""Simple ingestion example for newline-delimited JSON logs from Graph Digitizer. + +Reads logs/graph-digitizer.json by default and prints filtered entries. +Usage: + python ingest_json_log.py --level ERROR + python ingest_json_log.py --logger com.digitizer.core +""" +from __future__ import annotations +import argparse +import json +from pathlib import Path + +def parse_args(): + p = argparse.ArgumentParser(description="Ingest Graph Digitizer JSON log") + p.add_argument("--path", default="logs/graph-digitizer.json", help="Path to NDJSON log file") + p.add_argument("--level", help="Filter by level (INFO, DEBUG, WARN, ERROR)") + p.add_argument("--logger", help="Prefix filter for logger name") + return p.parse_args() + +def iter_events(path: Path): + with path.open("r", encoding="utf-8") as f: + for line in f: + line = line.strip() + if not line: + continue + try: + yield json.loads(line) + except json.JSONDecodeError: + continue # skip malformed + +def main(): + args = parse_args() + path = Path(args.path) + if not path.exists(): + print(f"Log file not found: {path}") + return 1 + count = 0 + for event in iter_events(path): + if args.level and event.get("level") != args.level: + continue + if args.logger and not event.get("logger", "").startswith(args.logger): + continue + print(f"{event.get('time')} {event.get('level'):>5} {event.get('logger')} - {event.get('message')}") + count += 1 + print(f"\nDisplayed {count} events") + return 0 + +if __name__ == "__main__": + raise SystemExit(main()) diff --git a/examples/scripts/select-icon.ps1 b/examples/scripts/select-icon.ps1 new file mode 100644 index 0000000..8059152 --- /dev/null +++ b/examples/scripts/select-icon.ps1 @@ -0,0 +1,38 @@ +<#! +.SYNOPSIS + Selects the best icon size from build/icons for a target packaging type. +.DESCRIPTION + Given a desired size (default 256) and fallback order, picks the closest available PNG or ICO. + Writes a properties file (selected-icon.properties) with icon.win, icon.mac, icon.linux entries. +.PARAMETER DesiredSize + Preferred size (e.g. 256, 512). +.PARAMETER Output + Properties file to write (default selected-icon.properties). +.EXAMPLE + pwsh scripts/select-icon.ps1 -DesiredSize 512 +#> +param( + [int]$DesiredSize = 256, + [string]$Output = "selected-icon.properties" +) +$iconDir = Join-Path (Get-Location) 'graph-digitizer-java/build/icons' +if (!(Test-Path -LiteralPath $iconDir)) { Write-Error "Icon directory not found: $iconDir"; exit 1 } +$files = Get-ChildItem -LiteralPath $iconDir -File | Where-Object { $_.Name -match 'scatter-plot-(\d+)\.(png|ico)$' } +if (-not $files) { Write-Error "No scatter-plot icons found"; exit 1 } +$parsed = $files | ForEach-Object { + if ($_.Name -match 'scatter-plot-(\d+)\.(png|ico)$') { + [pscustomobject]@{File=$_.FullName; Size=[int]$matches[1]; Ext=$_.Extension.ToLower()} + } +} | Sort-Object Size +# pick best match >= desired else largest below +$best = $parsed | Where-Object { $_.Size -ge $DesiredSize } | Select-Object -First 1 +if (-not $best) { $best = $parsed | Select-Object -Last 1 } +$linux = ($parsed | Where-Object { $_.Ext -eq '.png' -and $_.Size -ge $DesiredSize } | Select-Object -First 1) +if (-not $linux) { $linux = $parsed | Where-Object { $_.Ext -eq '.png' } | Select-Object -Last 1 } +$mac = $linux # placeholder until .icns generated externally +$win = ($parsed | Where-Object { $_.Ext -eq '.ico' -and $_.Size -ge $DesiredSize } | Select-Object -First 1) +if (-not $win) { $win = $parsed | Where-Object { $_.Ext -eq '.ico' } | Select-Object -Last 1 } +"icon.win=$($win.File)" | Out-File -FilePath $Output -Encoding UTF8 +"icon.mac=$($mac.File)" | Out-File -FilePath $Output -Encoding UTF8 -Append +"icon.linux=$($linux.File)" | Out-File -FilePath $Output -Encoding UTF8 -Append +Write-Host "Selected icons written to $Output" \ No newline at end of file diff --git a/examples/scripts/sign-macos.sh b/examples/scripts/sign-macos.sh new file mode 100644 index 0000000..e60df42 --- /dev/null +++ b/examples/scripts/sign-macos.sh @@ -0,0 +1,37 @@ +#!/usr/bin/env bash +# Code signing & notarization helper for macOS build artifacts. +# Requires environment variables: +# MAC_IDENTITY="Developer ID Application: Your Name (TEAMID)" +# MAC_NOTARIZE_APPLE_ID="appleid@example.com" +# MAC_NOTARIZE_TEAM_ID="TEAMID" +# MAC_NOTARIZE_PASS="app-specific-password" +# Usage: +# ./scripts/sign-macos.sh target/GraphDigitizer.app +set -euo pipefail +APP_PATH=${1:-target/GraphDigitizer.app} +if [ ! -d "$APP_PATH" ]; then + echo "App path not found: $APP_PATH" >&2; exit 1 +fi +: "${MAC_IDENTITY:?Missing MAC_IDENTITY}" +: "${MAC_NOTARIZE_APPLE_ID:?Missing MAC_NOTARIZE_APPLE_ID}" +: "${MAC_NOTARIZE_TEAM_ID:?Missing MAC_NOTARIZE_TEAM_ID}" +: "${MAC_NOTARIZE_PASS:?Missing MAC_NOTARIZE_PASS}" + +echo "Signing app bundle..." +codesign --deep --force --options runtime --sign "$MAC_IDENTITY" "$APP_PATH" + +echo "Verifying signature..." +codesign --verify --verbose=2 "$APP_PATH" +spctl --verbose=4 --assess --type execute "$APP_PATH" || echo "Gatekeeper assessment warning" + +echo "Creating ZIP for notarization..." +ZIP_NAME="$(basename "$APP_PATH").zip" +/usr/bin/ditto -c -k --keepParent "$APP_PATH" "$ZIP_NAME" + +echo "Submitting for notarization..." +xcrun notarytool submit "$ZIP_NAME" --apple-id "$MAC_NOTARIZE_APPLE_ID" --team-id "$MAC_NOTARIZE_TEAM_ID" --password "$MAC_NOTARIZE_PASS" --wait + +echo "Stapling ticket..." +xcrun stapler staple "$APP_PATH" + +echo "Done. Signed and notarized: $APP_PATH" diff --git a/examples/scripts/sign-windows.ps1 b/examples/scripts/sign-windows.ps1 new file mode 100644 index 0000000..78baa43 --- /dev/null +++ b/examples/scripts/sign-windows.ps1 @@ -0,0 +1,32 @@ +<#! +.SYNOPSIS + Code signs Windows executables produced by jpackage. +.DESCRIPTION + Wraps signtool.exe invocation. Requires a code signing certificate (.pfx) and password. +.PARAMETER CertPath + Path to PFX certificate file. +.PARAMETER Password + Password for the PFX (use ENV var or secret manager). +.PARAMETER TimestampUrl + RFC3161 timestamp server URL (default: https://timestamp.digicert.com). +.EXAMPLE + pwsh scripts/sign-windows.ps1 -CertPath certs/code-signing.pfx -Password $env:PFX_PW +#> +param( + [Parameter(Mandatory=$true)][string]$CertPath, + [Parameter(Mandatory=$true)][securestring]$Password, + [string]$TimestampUrl = 'https://timestamp.digicert.com' +) +$exeFiles = Get-ChildItem -Path 'graph-digitizer-java/target' -Filter '*.exe' -ErrorAction SilentlyContinue +if (-not $exeFiles) { Write-Warning 'No .exe files found to sign.'; exit 0 } +foreach ($f in $exeFiles) { + Write-Host "Signing $($f.Name)" + $bstr = [System.Runtime.InteropServices.Marshal]::SecureStringToBSTR($Password) + $pwPlain = [System.Runtime.InteropServices.Marshal]::PtrToStringBSTR($bstr) + try { + & signtool sign /fd SHA256 /f "$CertPath" /p "$pwPlain" /tr "$TimestampUrl" /td SHA256 "$($f.FullName)" || throw "Signing failed for $($f.Name)" + } finally { + [System.Runtime.InteropServices.Marshal]::ZeroFreeBSTR($bstr) + } +} +Write-Host 'Signing complete.' diff --git a/examples/scripts/verify-msi.ps1 b/examples/scripts/verify-msi.ps1 new file mode 100644 index 0000000..1e338a5 --- /dev/null +++ b/examples/scripts/verify-msi.ps1 @@ -0,0 +1,111 @@ +<# +verify-msi.ps1 + +A small smoke-test script to validate an MSI produced by jpackage. + +Features: +- Performs an administrative extract (msiexec /a) to a temp folder under target/jpackage-msi/extracted +- Locates the main EXE (GraphDigitizer.exe) inside the extracted tree +- Launches the EXE for a brief timeout and reports whether it started +- Writes a detailed log to target/jpackage-msi/verify-msi.log and returns non-zero on failure + +Usage: + pwsh .\scripts\verify-msi.ps1 + pwsh .\scripts\verify-msi.ps1 -MsiPath target\jpackage-msi\GraphDigitizer-1.1.msi -TimeoutSeconds 10 + +Optional parameter -DoInstall: if present the script will perform a normal install (msiexec /i) into a temporary folder and then uninstall it. +#> +param( + [string]$MsiPath = "target\jpackage-msi\GraphDigitizer-1.1.msi", + [int]$TimeoutSeconds = 10, + [switch]$DoInstall +) + +$cwd = (Get-Location).Path +$msi = (Resolve-Path $MsiPath -ErrorAction Stop).Path +$extract = Join-Path $cwd 'target\jpackage-msi\extracted' +$log = Join-Path $cwd 'target\jpackage-msi\verify-msi.log' + +function Log($msg){ "$((Get-Date).ToString('o')) - $msg" | Out-File -FilePath $log -Append } + +# Start fresh +if(Test-Path $log){ Remove-Item $log -Force -ErrorAction SilentlyContinue } +"=== VERIFY MSI SCRIPT START: $(Get-Date -Format o) ===" | Out-File -FilePath $log +Log "MSI: $msi" + +if(Test-Path $extract){ Remove-Item -Recurse -Force $extract -ErrorAction SilentlyContinue; Log "Removed existing extract dir: $extract" } +New-Item -ItemType Directory -Path $extract | Out-Null +Log "Extracting MSI to: $extract" + +$args = @('/a', $msi, '/qn', "TARGETDIR=$extract") +try{ + Start-Process -FilePath msiexec -ArgumentList $args -Wait -NoNewWindow -ErrorAction Stop + Log "Extraction completed OK" +} catch{ + Log "Extraction FAILED: $_" + exit 2 +} + +Log "Listing EXE files in extracted tree" +Get-ChildItem -Path $extract -Recurse -Filter *.exe | Sort-Object Length -Descending | Select-Object FullName,Length | Out-File -FilePath $log -Append + +$exeItem = Get-ChildItem -Path $extract -Recurse -Filter GraphDigitizer.exe | Select-Object -First 1 +if(-not $exeItem){ + Log "ERROR: GraphDigitizer.exe not found in extracted tree" + exit 3 +} +$exe = $exeItem.FullName +Log "Found extracted EXE: $exe" + +# Start the EXE and wait briefly +try{ + $p = Start-Process -FilePath $exe -PassThru -ErrorAction Stop + Log "Started process PID: $($p.Id)" + $waitMillis = [int]($TimeoutSeconds * 1000) + $exited = $p.WaitForExit($waitMillis) + if($exited){ + Log "Process exited within ${waitMillis}ms. ExitCode: $($p.ExitCode)" + } else { + Log "Process did not exit within ${waitMillis}ms; attempting to close." + try{ $p.Kill(); Log "Process killed" } catch{ Log "Failed to kill process: $_" } + } +} catch{ + Log "Failed to start EXE: $_" + exit 4 +} + +if($DoInstall){ + # perform a normal install to a temp folder and uninstall it to validate installer actions + $installDir = Join-Path $cwd 'target\jpackage-msi\installed' + if(Test-Path $installDir){ Remove-Item -Recurse -Force $installDir -ErrorAction SilentlyContinue } + New-Item -ItemType Directory -Path $installDir | Out-Null + Log "Performing silent install to: $installDir" + $args = @('/i', $msi, '/qn', "TARGETDIR=$installDir", '/l*v', (Join-Path $cwd 'target\jpackage-msi\install.log')) + try{ + Start-Process -FilePath msiexec -ArgumentList $args -Wait -NoNewWindow -ErrorAction Stop + Log "Silent install completed" + # find installed exe and run briefly + $installedExe = Get-ChildItem -Path $installDir -Recurse -Filter GraphDigitizer.exe | Select-Object -First 1 + if($installedExe){ + Log "Found installed exe: $($installedExe.FullName)" + $p2 = Start-Process -FilePath $installedExe.FullName -PassThru -ErrorAction Stop + $ran = $p2.WaitForExit([int]($TimeoutSeconds*1000)) + if($ran){ Log "Installed process exited, ExitCode: $($p2.ExitCode)" } else { $p2.Kill(); Log "Installed process killed" } + } else { + Log "Installed exe not found under $installDir" + } + # uninstall by product code is more robust; try to use UninstallString if present in registry + Log "Attempting uninstall by TARGETDIR cleanup (best-effort)" + Remove-Item -Recurse -Force $installDir -ErrorAction SilentlyContinue + Log "Cleanup of installDir attempted" + } catch{ + Log "Silent install FAILED: $_" + exit 5 + } +} + +Log "=== VERIFY MSI SCRIPT END: $(Get-Date -Format o) ===" +Write-Output "Verification log: $log" +Get-Content $log -Tail 200 | Write-Output + +exit 0 diff --git a/json_Files/students.json.txt b/json_Files/students.json.txt index 1f8b31b..768600e 100644 --- a/json_Files/students.json.txt +++ b/json_Files/students.json.txt @@ -1,30 +1,30 @@ -{ - "students": [ - "Aaron A Aaronsson", - "Baron B Baronsson", - "Cecilia C Cecilsson", - "Derek D Dereksson", - "Eva E Evasson" - "Fiona F Fionasson", - "George G Georgesson", - "Hannah H Hannahsson", - "Ian I Iansson", - "Jenna J Jennasson" - "Kevin K Kevansson", - "Luna L Lunasson", - "Mason M Masonsson", - "Nina N Ninasson", - "Owen O Owensson", - "Paula P Paulasson", - "Quinn Q Quinnsson", - "Riley R Rileysson", - "Sophia S Sophiasson", - "Tyler T Tylersson", - "Uma U Umasson", - "Violet V Violetsson", - "William W Williamsson", - "Xander X Xandersson", - "Yara Y Yarasson", - "Zane Z Zanesson" - ] -} +{ + "students": [ + "Aaron A Aaronsson", + "Baron B Baronsson", + "Cecilia C Cecilsson", + "Derek D Dereksson", + "Eva E Evasson" + "Fiona F Fionasson", + "George G Georgesson", + "Hannah H Hannahsson", + "Ian I Iansson", + "Jenna J Jennasson" + "Kevin K Kevansson", + "Luna L Lunasson", + "Mason M Masonsson", + "Nina N Ninasson", + "Owen O Owensson", + "Paula P Paulasson", + "Quinn Q Quinnsson", + "Riley R Rileysson", + "Sophia S Sophiasson", + "Tyler T Tylersson", + "Uma U Umasson", + "Violet V Violetsson", + "William W Williamsson", + "Xander X Xandersson", + "Yara Y Yarasson", + "Zane Z Zanesson" + ] +} diff --git a/nbactions.xml b/nbactions.xml index b89d19f..85053a6 100644 --- a/nbactions.xml +++ b/nbactions.xml @@ -1,20 +1,20 @@ - - - - run - - jar - - - process-classes - org.codehaus.mojo:exec-maven-plugin:3.1.0:exec - - - - ${exec.vmArgs} -classpath %classpath ${exec.mainClass} ${exec.appArgs} - - Main - java - - - + + + + run + + jar + + + process-classes + org.codehaus.mojo:exec-maven-plugin:3.1.0:exec + + + + ${exec.vmArgs} -classpath %classpath ${exec.mainClass} ${exec.appArgs} + + Main + java + + + diff --git a/pom.xml b/pom.xml index a4d3b38..27340a3 100644 --- a/pom.xml +++ b/pom.xml @@ -1,246 +1,300 @@ - - - 4.0.0 - - com.example - Main - 1.0.0-beta - - - UTF-8 - 21 - - - - - org.jfree - jfreechart - 1.5.5 - - - org.xerial - sqlite-jdbc - 3.46.0.1 - - - com.itextpdf - itextpdf - 5.5.13.4 - - - com.formdev - flatlaf - 3.5.1 - - - com.formdev - flatlaf-intellij-themes - 3.5.1 - - - org.slf4j - slf4j-api - 2.0.16 - - - - com.fasterxml.jackson.core - jackson-databind - 2.15.2 - - - ch.qos.logback - logback-classic - 1.5.7 - - - ch.qos.logback - logback-core - 1.5.7 - - - - org.junit.jupiter - junit-jupiter - 5.10.0 - test - - - - vision-skills-progression-tracker-${project.version} - - - true - maven-javadoc-plugin - 3.8.0 - - - attach-javadocs - - jar - - - - - - org.apache.maven.plugins - maven-resources-plugin - 3.3.1 - - - copy-resources - prepare-package - - resources - - - - - src/main/resources - - version.properties - - true - - - ${project.build.directory}/classes - - - - - - org.apache.maven.plugins - maven-shade-plugin - 3.6.0 - - - package - - shade - - - - - *:* - - module-info.class - - - - *:* - - META-INF/MANIFEST.MF - META-INF/LICENSE - META-INF/LICENSE.txt - META-INF/NOTICE - META-INF/NOTICE.txt - META-INF/versions/9/module-info.class - - - - - - - com.studentgui.bootstrap.Bootstrap - - Michael Ryan Hunsaker, M.Ed., Ph.D. - - - - - - - - - - org.apache.maven.plugins - maven-surefire-plugin - 3.0.0-M7 - - false - - - - - org.apache.maven.plugins - maven-checkstyle-plugin - 3.4.0 - - - false - true - - config/checkstyle/checkstyle.xml - - - - checkstyle - verify - - check - - - - - - com.github.spotbugs - spotbugs-maven-plugin - 4.7.3.2 - - Max - Low - false - true - - - - spotbugs - verify - - check - - - - - - - - - - org.apache.maven.plugins - maven-checkstyle-plugin - 3.4.0 - - - - checkstyle - - - - - - - Vision Skills Progression Tracker - Vision Skills Progression Tracker - - - Apache-2.0 - https://www.apache.org/licenses/LICENSE-2.0.txt - manual - A business-friendly OSS license - - - - Michael Ryan Hunsaker, M.Ed., Ph.D. - https://github.com/mrhunsaker/ - - - - mrhunsaker - Michael Ryan Hunsaker, M.Ed., Ph.D. - hunsakerconsulting@gmail.com - - - + + + 4.0.0 + + com.example + Main + 1.0.0-beta + + + UTF-8 + 21 + + + + + org.jfree + jfreechart + 1.5.5 + + + org.xerial + sqlite-jdbc + 3.46.0.1 + + + com.itextpdf + itextpdf + 5.5.13.4 + + + com.formdev + flatlaf + 3.5.1 + + + com.formdev + flatlaf-intellij-themes + 3.5.1 + + + org.slf4j + slf4j-api + 2.0.16 + + + + com.fasterxml.jackson.core + jackson-databind + 2.15.2 + + + ch.qos.logback + logback-classic + 1.5.7 + + + ch.qos.logback + logback-core + 1.5.7 + + + + org.junit.jupiter + junit-jupiter + 5.10.0 + test + + + + StudentDataGUI-${project.version} + + + true + maven-javadoc-plugin + 3.8.0 + + ${project.build.sourceEncoding} + protected + false + none + -Xdoclint:none + ${maven.compiler.release} + true + true + + + + attach-javadocs + + jar + + + + + + org.apache.maven.plugins + maven-resources-plugin + 3.3.1 + + + copy-resources + prepare-package + + resources + + + + + src/main/resources + + version.properties + + true + + + ${project.build.directory}/classes + + + + + + org.apache.maven.plugins + maven-shade-plugin + 3.6.0 + + + package + + shade + + + + + *:* + + module-info.class + + + + *:* + + META-INF/MANIFEST.MF + META-INF/LICENSE + META-INF/LICENSE.txt + META-INF/NOTICE + META-INF/NOTICE.txt + META-INF/versions/9/module-info.class + + + + + + + com.studentgui.bootstrap.Bootstrap + + Michael Ryan Hunsaker, M.Ed., Ph.D. + + + + + + + + + + org.apache.maven.plugins + maven-surefire-plugin + 3.0.0-M7 + + false + + + + + org.apache.maven.plugins + maven-checkstyle-plugin + 3.4.0 + + + false + true + + config/checkstyle/checkstyle.xml + + + + checkstyle + verify + + check + + + + + + com.github.spotbugs + spotbugs-maven-plugin + 4.7.3.2 + + Max + Low + false + true + + + + spotbugs + verify + + check + + + + + + + + + + org.apache.maven.plugins + maven-project-info-reports-plugin + 3.4.5 + + + + index + summary + licenses + scm + dependencies + dependency-info + dependency-management + modules + plugins + + + + + + org.apache.maven.plugins + maven-javadoc-plugin + 3.8.0 + + ${project.build.sourceEncoding} + none + false + true + true + + + + org.apache.maven.plugins + maven-surefire-report-plugin + 3.5.0 + + + + report + + + + + + org.apache.maven.plugins + maven-checkstyle-plugin + 3.4.0 + + + + checkstyle + + + + + + + Vision Skills Progression Tracker + Vision Skills Progression Tracker + + + Apache-2.0 + https://www.apache.org/licenses/LICENSE-2.0.txt + manual + A business-friendly OSS license + + + + Michael Ryan Hunsaker, M.Ed., Ph.D. + https://github.com/mrhunsaker/ + + + + mrhunsaker + Michael Ryan Hunsaker, M.Ed., Ph.D. + hunsakerconsulting@gmail.com + + + diff --git a/src/main/java/.gitkeep b/src/main/java/.gitkeep index 78676ae..4f577f4 100644 --- a/src/main/java/.gitkeep +++ b/src/main/java/.gitkeep @@ -1,3 +1,3 @@ -# .gitkeep placeholder to avoid committing empty directories -# Root-level placeholder files (e.g., Abacus.java, Braille.java) were removed -# Packaged classes live under `com/studentgui/apppages` and are the canonical sources. +# .gitkeep placeholder to avoid committing empty directories +# Root-level placeholder files (e.g., Abacus.java, Braille.java) were removed +# Packaged classes live under `com/studentgui/apppages` and are the canonical sources. diff --git a/src/main/java/Abacus.java b/src/main/java/Abacus.java index 4c9bcd8..caf5064 100644 --- a/src/main/java/Abacus.java +++ b/src/main/java/Abacus.java @@ -1,2 +1,2 @@ -// Removed root-level placeholder for Abacus.java. -// The canonical class lives in package com.studentgui.apppages and is the source of truth. +// Removed root-level placeholder for Abacus.java. +// The canonical class lives in package com.studentgui.apppages and is the source of truth. diff --git a/src/main/java/Braille.java b/src/main/java/Braille.java index 5371183..25755dc 100644 --- a/src/main/java/Braille.java +++ b/src/main/java/Braille.java @@ -1,3 +1,3 @@ -// Removed root-level placeholder for Braille.java. -// The canonical class lives in package com.studentgui.apppages and is the source of truth. - +// Removed root-level placeholder for Braille.java. +// The canonical class lives in package com.studentgui.apppages and is the source of truth. + diff --git a/src/main/java/BrailleNote.java b/src/main/java/BrailleNote.java index 2ef794a..32183b8 100644 --- a/src/main/java/BrailleNote.java +++ b/src/main/java/BrailleNote.java @@ -1,2 +1,2 @@ -// Removed root-level placeholder for BrailleNote.java. -// The canonical class lives in package com.studentgui.apppages and is the source of truth. +// Removed root-level placeholder for BrailleNote.java. +// The canonical class lives in package com.studentgui.apppages and is the source of truth. diff --git a/src/main/java/BrailleSense.java b/src/main/java/BrailleSense.java index 06c5281..eab3aff 100644 --- a/src/main/java/BrailleSense.java +++ b/src/main/java/BrailleSense.java @@ -1,2 +1,2 @@ -// Removed root-level placeholder for BrailleSense.java. -// The canonical class lives in package com.studentgui.apppages and is the source of truth. +// Removed root-level placeholder for BrailleSense.java. +// The canonical class lives in package com.studentgui.apppages and is the source of truth. diff --git a/src/main/java/CVI.java b/src/main/java/CVI.java index c90739a..dbbe143 100644 --- a/src/main/java/CVI.java +++ b/src/main/java/CVI.java @@ -1,2 +1,2 @@ -// Removed root-level placeholder for CVI.java. -// The canonical class lives in package com.studentgui.apppages and is the source of truth. +// Removed root-level placeholder for CVI.java. +// The canonical class lives in package com.studentgui.apppages and is the source of truth. diff --git a/src/main/java/DigitalLiteracy.java b/src/main/java/DigitalLiteracy.java index f9a95f4..c9de33e 100644 --- a/src/main/java/DigitalLiteracy.java +++ b/src/main/java/DigitalLiteracy.java @@ -1,2 +1,2 @@ -// Removed root-level placeholder for DigitalLiteracy.java. -// The canonical class lives in package com.studentgui.apppages and is the source of truth. +// Removed root-level placeholder for DigitalLiteracy.java. +// The canonical class lives in package com.studentgui.apppages and is the source of truth. diff --git a/src/main/java/IOS.java b/src/main/java/IOS.java index b0305f1..d4ce68e 100644 --- a/src/main/java/IOS.java +++ b/src/main/java/IOS.java @@ -1,2 +1,2 @@ -// Removed root-level placeholder for IOS.java. -// The canonical class lives in package com.studentgui.apppages and is the source of truth. +// Removed root-level placeholder for IOS.java. +// The canonical class lives in package com.studentgui.apppages and is the source of truth. diff --git a/src/main/java/Keyboarding.java b/src/main/java/Keyboarding.java index d42a9f4..930245a 100644 --- a/src/main/java/Keyboarding.java +++ b/src/main/java/Keyboarding.java @@ -1,2 +1,2 @@ -// Removed root-level placeholder for Keyboarding.java. -// The canonical class lives in package com.studentgui.apppages and is the source of truth. +// Removed root-level placeholder for Keyboarding.java. +// The canonical class lives in package com.studentgui.apppages and is the source of truth. diff --git a/src/main/java/Main.java b/src/main/java/Main.java index 8bae165..cc02bd8 100644 --- a/src/main/java/Main.java +++ b/src/main/java/Main.java @@ -1,5 +1,5 @@ -// Removed legacy content. This file intentionally left with a harmless placeholder. - -final class PlaceholderMain { - // nothing here -} +// Removed legacy content. This file intentionally left with a harmless placeholder. + +final class PlaceholderMain { + // nothing here +} diff --git a/src/main/java/ScreenReader.java b/src/main/java/ScreenReader.java index acc27b7..a2cb3ae 100644 --- a/src/main/java/ScreenReader.java +++ b/src/main/java/ScreenReader.java @@ -1,2 +1,2 @@ -// Removed root-level placeholder for ScreenReader.java. -// The canonical class lives in package com.studentgui.apppages and is the source of truth. +// Removed root-level placeholder for ScreenReader.java. +// The canonical class lives in package com.studentgui.apppages and is the source of truth. diff --git a/src/main/java/VersionUtil.java b/src/main/java/VersionUtil.java index 3f18560..dd592a1 100644 --- a/src/main/java/VersionUtil.java +++ b/src/main/java/VersionUtil.java @@ -1,69 +1,69 @@ -import java.io.IOException; -import java.io.InputStream; -import java.util.Properties; - -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; - -/** - * Utility to surface project version information. - * - * Reads the {@code /version.properties} file from the classpath and exposes - * the {@link #getVersion()} helper. If the file cannot be read, returns - * {@code "unknown"}. - */ -public class VersionUtil { - private static final Logger LOG = LoggerFactory.getLogger(VersionUtil.class); - - /** The path to the properties file containing the version information. */ - private static final String VERSION_FILE = "/version.properties"; - - /** The version of the application, initialized from the properties file. */ - private static String version; - - /** - * Static block to initialize the {@link #version} variable. - *

- * The static block loads the version from the {@code version.properties} file. - * If the file cannot be found or an I/O error occurs, the version is set to "unknown". - *

- */ - static { - try (InputStream input = VersionUtil.class.getResourceAsStream(VERSION_FILE)) { - Properties properties = new Properties(); - if (input == null) { - // If the properties file is not found, set version to "unknown" - LOG.warn("Unable to find {}", VERSION_FILE); - version = "unknown"; - } else { - // Load the properties file and set the version - properties.load(input); - version = properties.getProperty("version", "unknown"); - } - } catch (IOException ex) { - // Log the exception and set version to "unknown" in case of an exception - LOG.error("Error reading version properties", ex); - version = "unknown"; - } - } - - /** - * Returns the version of the application. - *

- * This method provides access to the version information that was loaded from the properties file. - * If the properties file could not be found or an error occurred, it returns "unknown". - *

- * - * @return The version of the application. - */ - public static String getVersion() { - return version; - } - - /** - * Private constructor to prevent instantiation of this utility class. - */ - private VersionUtil() { - throw new AssertionError("Not instantiable"); - } -} +import java.io.IOException; +import java.io.InputStream; +import java.util.Properties; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + * Utility to surface project version information. + * + * Reads the {@code /version.properties} file from the classpath and exposes + * the {@link #getVersion()} helper. If the file cannot be read, returns + * {@code "unknown"}. + */ +public class VersionUtil { + private static final Logger LOG = LoggerFactory.getLogger(VersionUtil.class); + + /** The path to the properties file containing the version information. */ + private static final String VERSION_FILE = "/version.properties"; + + /** The version of the application, initialized from the properties file. */ + private static String version; + + /** + * Static block to initialize the {@link #version} variable. + *

+ * The static block loads the version from the {@code version.properties} file. + * If the file cannot be found or an I/O error occurs, the version is set to "unknown". + *

+ */ + static { + try (InputStream input = VersionUtil.class.getResourceAsStream(VERSION_FILE)) { + Properties properties = new Properties(); + if (input == null) { + // If the properties file is not found, set version to "unknown" + LOG.warn("Unable to find {}", VERSION_FILE); + version = "unknown"; + } else { + // Load the properties file and set the version + properties.load(input); + version = properties.getProperty("version", "unknown"); + } + } catch (IOException ex) { + // Log the exception and set version to "unknown" in case of an exception + LOG.error("Error reading version properties", ex); + version = "unknown"; + } + } + + /** + * Returns the version of the application. + *

+ * This method provides access to the version information that was loaded from the properties file. + * If the properties file could not be found or an error occurred, it returns "unknown". + *

+ * + * @return The version of the application. + */ + public static String getVersion() { + return version; + } + + /** + * Private constructor to prevent instantiation of this utility class. + */ + private VersionUtil() { + throw new AssertionError("Not instantiable"); + } +} diff --git a/src/main/java/com/studentgui/app/DateChangeListener.java b/src/main/java/com/studentgui/app/DateChangeListener.java index 5e927bb..009f180 100644 --- a/src/main/java/com/studentgui/app/DateChangeListener.java +++ b/src/main/java/com/studentgui/app/DateChangeListener.java @@ -1,15 +1,15 @@ -package com.studentgui.app; - -import java.time.LocalDate; - -/** - * Simple listener interface for pages that want to be notified when the - * application-wide selected date changes via the top-bar Apply action. - */ -public interface DateChangeListener { - /** - * Called when the application date has been changed by the user. - * @param newDate the newly selected date - */ - void dateChanged(LocalDate newDate); -} +package com.studentgui.app; + +import java.time.LocalDate; + +/** + * Simple listener interface for pages that want to be notified when the + * application-wide selected date changes via the top-bar Apply action. + */ +public interface DateChangeListener { + /** + * Called when the application date has been changed by the user. + * @param newDate the newly selected date + */ + void dateChanged(LocalDate newDate); +} diff --git a/src/main/java/com/studentgui/app/Main.java b/src/main/java/com/studentgui/app/Main.java index 0d6c87f..228300c 100644 --- a/src/main/java/com/studentgui/app/Main.java +++ b/src/main/java/com/studentgui/app/Main.java @@ -1,597 +1,597 @@ -package com.studentgui.app; - -import java.awt.BorderLayout; -import java.awt.CardLayout; -import java.awt.FlowLayout; -import java.time.LocalDate; -import java.time.format.DateTimeParseException; -import java.util.List; - -import javax.swing.JButton; -import javax.swing.JComboBox; -import javax.swing.JComponent; -import javax.swing.JFrame; -import javax.swing.JLabel; -import javax.swing.JPanel; -import javax.swing.JTextField; -import javax.swing.SwingUtilities; -import javax.swing.UIManager; - -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; - -import com.studentgui.apphelpers.Helpers; -import com.studentgui.apphelpers.SqlGenerate; -import com.studentgui.apppages.Abacus; -import com.studentgui.apppages.Braille; -import com.studentgui.apppages.BrailleNote; -import com.studentgui.apppages.BrailleSense; -import com.studentgui.apppages.CVI; -import com.studentgui.apppages.ContactLog; -import com.studentgui.apppages.DigitalLiteracy; -import com.studentgui.apppages.Homepage; -import com.studentgui.apppages.IOS; -import com.studentgui.apppages.InstructionalMaterials; -import com.studentgui.apppages.JLineGraph; -import com.studentgui.apppages.Keyboarding; -import com.studentgui.apppages.Observations; -import com.studentgui.apppages.ScreenReader; -import com.studentgui.apppages.SessionNotes; -import com.studentgui.apptheming.Theme; - -/** - * Main application entry and UI wiring for the Student Skills Progressions app. - * - * This class builds the top-level window, menu, and registers the skill pages - * (each page is a JPanel). It's intentionally lightweight; most functionality - * for database access and page logic lives in helper classes and the page - * components under com.studentgui.apppages. - */ -/** - * Application bootstrap and top-level UI wiring. Builds the main JFrame, - * registers pages, and provides a small top control bar for switching - * students and pages. - */ -/** - * Application bootstrap and top-level UI wiring. Builds the main JFrame, - * registers pages, and provides a small top control bar for switching - * students and pages. - */ -/** - * Application entry point and top-level UI wiring for the Student Skills - * Progressions application. Builds the main frame, menu and registers per-page - * panels under a CardLayout. - */ -public class Main { - /** - * Bootstrap logging/system properties very early so Logback can resolve - * file locations and the per-run timestamp before any logger is - * initialized. This static block sets APP_HOME and LOG_TS and performs - * a cleanup of old log files older than 7 days. - */ - static { - try { - // Ensure Helpers.APP_HOME is initialized and use it for logging - String appHome = com.studentgui.apphelpers.Helpers.APP_HOME.toString(); - System.setProperty("APP_HOME", appHome); - // unix epoch seconds appended to per-run log filename - String ts = String.valueOf(java.time.Instant.now().getEpochSecond()); - System.setProperty("LOG_TS", ts); - - // create logs dir - java.nio.file.Path logs = java.nio.file.Paths.get(appHome).resolve("logs"); - java.nio.file.Files.createDirectories(logs); - - // Cleanup: remove log files older than 7 days (by last modified time) - long cutoff = java.time.Instant.now().minus(java.time.Duration.ofDays(7)).toEpochMilli(); - try (java.nio.file.DirectoryStream ds = java.nio.file.Files.newDirectoryStream(logs, "log_*.log")) { - for (java.nio.file.Path p : ds) { - try { - java.nio.file.attribute.FileTime ft = java.nio.file.Files.getLastModifiedTime(p); - if (ft.toMillis() < cutoff) { - java.nio.file.Files.deleteIfExists(p); - } - } catch (Exception ex) { - // Swallow cleanup exceptions; logging isn't available yet. - } - } - } catch (Exception ex) { - // ignore - } - // also remove consolidated data dump files older than retention - try (java.nio.file.DirectoryStream ds2 = java.nio.file.Files.newDirectoryStream(logs, "data_dumps_*.log")) { - for (java.nio.file.Path p : ds2) { - try { - java.nio.file.attribute.FileTime ft = java.nio.file.Files.getLastModifiedTime(p); - if (ft.toMillis() < cutoff) { - java.nio.file.Files.deleteIfExists(p); - } - } catch (Exception ex) { - // Swallow cleanup exceptions; logging isn't available yet. - } - } - } catch (Exception ex) { - // ignore - } - } catch (Exception ex) { - // If anything here fails, continue — logging may not be configured yet. - } - } - - private static final Logger LOG = LoggerFactory.getLogger(Main.class); - private static JFrame frame; - private static JPanel contentPanel; - private static JLineGraph sharedGraph; - /** - * Shared JLineGraph instance used across pages. - * - * Pages are constructed with the shared graph passed into their - * constructors (see recreatePages). The shared graph is registered - * with {@link #addSettingsChangeListener(SettingsChangeListener)} so - * it receives runtime preference updates. If a page creates its own - * page-local JLineGraph instance it should register it with - * {@link #addSettingsChangeListener(SettingsChangeListener)} and remove - * it when disposed to ensure it receives preference changes and to - * avoid leaking listeners. - */ - // current date used by the top bar (can be updated without recreating pages) - private static java.time.LocalDate currentDate; - private static String currentStudent; - // Listeners to notify when the top-bar date changes - private static final java.util.List dateListeners = new java.util.concurrent.CopyOnWriteArrayList<>(); - - /** - * Register a listener to be notified when the application date is changed via the top bar. - * - * @param l listener to register (ignored when null) - */ - public static void addDateChangeListener(final DateChangeListener l) { - if (l != null) { - dateListeners.add(l); - } - } - - /** - * Remove a previously registered date change listener. - * - * @param l listener to remove (ignored when null) - */ - public static void removeDateChangeListener(final DateChangeListener l) { - if (l != null) { - dateListeners.remove(l); - } - } - - /** - * Clear all registered date change listeners. - */ - public static void clearDateChangeListeners() { - dateListeners.clear(); - } - - /** - * Notify all registered date listeners that the application date has changed. - * - * @param d new application date - */ - private static void notifyDateChanged(final java.time.LocalDate d) { - for (DateChangeListener l : dateListeners) { - try { - l.dateChanged(d); - } catch (Exception ex) { - LOG.warn("DateChangeListener threw: {}", ex.toString()); - } - } - } - // Student change listeners - private static final java.util.List studentListeners = new java.util.concurrent.CopyOnWriteArrayList<>(); - /** - * Register a listener to be notified when the selected student is changed. - * - * @param l listener to register (ignored when null) - */ - public static void addStudentChangeListener(final StudentChangeListener l) { - if (l != null) { - studentListeners.add(l); - } - } - - /** - * Remove a previously registered student change listener. - * - * @param l listener to remove (ignored when null) - */ - public static void removeStudentChangeListener(final StudentChangeListener l) { - if (l != null) { - studentListeners.remove(l); - } - } - - /** - * Clear all registered student change listeners. - */ - public static void clearStudentChangeListeners() { - studentListeners.clear(); - } - - /** - * Notify registered student change listeners that the selected student has changed. - * - * @param s new selected student name - */ - private static void notifyStudentChanged(final String s) { - currentStudent = s; - for (StudentChangeListener l : studentListeners) { - try { - l.studentChanged(s); - } catch (Exception ex) { - LOG.warn("StudentChangeListener threw: {}", ex.toString()); - } - } - } - - - // Settings change listeners - private static final java.util.List settingsListeners = new java.util.concurrent.CopyOnWriteArrayList<>(); - - /** - * Register a listener to be notified when application settings change. - * Implementations should read values from {@link com.studentgui.apphelpers.Settings} - * when {@link SettingsChangeListener#settingsChanged()} is invoked. - * - * @param l listener to register (ignored when null) - */ - public static void addSettingsChangeListener(final SettingsChangeListener l) { - if (l != null) { - settingsListeners.add(l); - } - } - - /** - * Remove a previously registered settings change listener. - * - * @param l listener to remove (ignored when null) - */ - public static void removeSettingsChangeListener(final SettingsChangeListener l) { - if (l != null) { - settingsListeners.remove(l); - } - } - - /** - * Clear all registered settings change listeners. - */ - public static void clearSettingsChangeListeners() { - settingsListeners.clear(); - } - - /** - * Notify all registered settings listeners that application settings have been changed. - * This is typically invoked after persisting preferences through - * {@link com.studentgui.apphelpers.Settings}. - */ - public static void notifySettingsChanged() { - for (SettingsChangeListener l : settingsListeners) { - try { - l.settingsChanged(); - } catch (Exception ex) { - LOG.warn("SettingsChangeListener threw: {}", ex.toString()); - } - } - } - - /** - * Application entry point. Initializes helpers, database, and launches the - * Swing UI on the EDT. - * - * @param args command-line arguments (unused) - */ - public static void main(final String[] args) { - // Apply saved look and feel (default to light) - // Settings.get and setTheme handle any expected failures internally; - // call directly so we avoid a broad RuntimeException catch. - String saved = com.studentgui.apphelpers.Settings.get("theme", "light"); - setTheme(saved); - - // Initialize helpers and DB - Helpers.setStartDir(); - Helpers.createFolderHierarchy(); - SqlGenerate.initializeDatabase(); - - SwingUtilities.invokeLater(() -> { - frame = new JFrame("Student Skills Progressions"); - frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE); - frame.setSize(1000, 700); - frame.setLocationRelativeTo(null); - - // Menu bar: obtain the app menu bar from Theme, insert a File->Exit menu at the far left - javax.swing.JMenuBar themeBar = Theme.createMenuBar(); - if (themeBar == null) { - themeBar = new javax.swing.JMenuBar(); - } - javax.swing.JMenu fileMenu = new javax.swing.JMenu("File"); - javax.swing.JMenuItem exitItem = new javax.swing.JMenuItem("Exit"); - exitItem.addActionListener(e -> { - LOG.info("Exit requested via File->Exit"); - if (frame != null) { - frame.dispose(); - } - System.exit(0); - }); - fileMenu.add(exitItem); - // Insert file menu at position 0 so it appears on the far left - themeBar.add(fileMenu, 0); - // Ensure the Themes menu (if present) appears immediately after File - int themesIdx = -1; - for (int i = 0; i < themeBar.getMenuCount(); i++) { - javax.swing.JMenu m = themeBar.getMenu(i); - if (m != null && "Themes".equals(m.getText())) { themesIdx = i; break; } - } - if (themesIdx > 1) { - javax.swing.JMenu themesMenu = themeBar.getMenu(themesIdx); - themeBar.remove(themesIdx); - themeBar.add(themesMenu, 1); - } - frame.setJMenuBar(themeBar); - - - contentPanel = new JPanel(new CardLayout()); - frame.add(contentPanel, BorderLayout.CENTER); - - // Top control bar: student selector, date, and navigation - JPanel topBar = buildTopBar(); - frame.add(topBar, BorderLayout.NORTH); - - // Create initial shared graph and pages for the first student - sharedGraph = new JLineGraph(); - // Register shared graph to receive settings change notifications - addSettingsChangeListener(sharedGraph); - List students = Helpers.getStudents(); - String demoStudent = students.isEmpty() ? "Demo Student" : students.get(0); - LocalDate today = LocalDate.now(); - currentDate = today; - recreatePages(demoStudent, today); - - frame.setVisible(true); - }); - } - - /** - * Change application theme at runtime. Supported values: "light", "dark", "darcula". - * This method updates the installed Look and Feel and refreshes the main frame. - * - * @param theme human-friendly theme name or fully-qualified LookAndFeel class name - */ - public static void setTheme(final String theme) { - try { - String t = theme == null ? "light" : theme; - // Common keywords for bundled themes - switch (t.toLowerCase()) { - case "dark": - case "flatdarklaf": - UIManager.setLookAndFeel(new com.formdev.flatlaf.FlatDarkLaf()); - break; - case "darcula": - // Darcula-like: use FlatDarkLaf as fallback - UIManager.setLookAndFeel(new com.formdev.flatlaf.FlatDarkLaf()); - break; - case "light": - case "flatlightlaf": - UIManager.setLookAndFeel(new com.formdev.flatlaf.FlatLightLaf()); - break; - default: - // If the string looks like a fully-qualified class name, try to set it directly. - if (t.contains(".")) { - try { - UIManager.setLookAndFeel(t); - } catch (ReflectiveOperationException | javax.swing.UnsupportedLookAndFeelException ex) { - // Try to instantiate via reflection - try { - Class c = Class.forName(t); - Object o = c.getDeclaredConstructor().newInstance(); - if (o instanceof javax.swing.LookAndFeel) { - UIManager.setLookAndFeel((javax.swing.LookAndFeel) o); - } else { - // fallback to light - UIManager.setLookAndFeel(new com.formdev.flatlaf.FlatLightLaf()); - } - } catch (ReflectiveOperationException | javax.swing.UnsupportedLookAndFeelException ex2) { - LOG.error("Failed to set look and feel by class name {}", t, ex2); - UIManager.setLookAndFeel(new com.formdev.flatlaf.FlatLightLaf()); - } - } - } else { - // Try to find an installed LAF by name - boolean applied = false; - for (UIManager.LookAndFeelInfo info : UIManager.getInstalledLookAndFeels()) { - if (info.getName().equalsIgnoreCase(t) || info.getName().toLowerCase().contains(t.toLowerCase())) { - UIManager.setLookAndFeel(info.getClassName()); - applied = true; - break; - } - } - if (!applied) { - // default fallback - UIManager.setLookAndFeel(new com.formdev.flatlaf.FlatLightLaf()); - } - } - break; - } - if (frame != null) { - javax.swing.SwingUtilities.updateComponentTreeUI(frame); - frame.pack(); - } - } catch (ReflectiveOperationException | javax.swing.UnsupportedLookAndFeelException | IllegalArgumentException e) { - LOG.error("Failed to set theme {}", theme, e); - } - } - - private static JPanel buildTopBar() { - JPanel bar = new JPanel(new FlowLayout(FlowLayout.LEFT)); - List students = Helpers.getStudents(); - JComboBox studentBox = new JComboBox<>(students.toArray(new String[0])); - studentBox.setEditable(false); - - JLabel dateLabel = new JLabel("Date (YYYY-MM-DD):"); - JTextField dateField = new JTextField(LocalDate.now().toString(), 10); - - JButton goBtn = new JButton("Apply"); - - bar.add(new JLabel("Student:")); - bar.add(studentBox); - bar.add(dateLabel); - bar.add(dateField); - bar.add(goBtn); - - goBtn.addActionListener(e -> { - String selected = (String) studentBox.getSelectedItem(); - LocalDate date = LocalDate.now(); - try { - date = LocalDate.parse(dateField.getText()); - } catch (DateTimeParseException ex) { - // keep today - } - // Update the app's current date and selected student without recreating pages; show a confirmation dialog. - currentDate = date; - currentStudent = selected; - javax.swing.JOptionPane.showMessageDialog(frame, - "The date has been updated to " + date.toString(), - "Date Updated", - javax.swing.JOptionPane.INFORMATION_MESSAGE); - // Notify registered pages so they can update any internal state - notifyDateChanged(date); - notifyStudentChanged(selected); - }); - - // Navigation buttons removed from top bar per UI request; pages can still be selected via menu - - return bar; - } - - /** - * Recreate per-page panels for the provided student and date. This replaces - * the CardLayout content so the shared graph and pages are reset. - */ - /** - * Recreate per-page panels for the provided student and date. This - * replaces the CardLayout content so the shared graph and pages are reset. - * - * @param student selected student's display name - * @param date the session date for newly created pages - */ - private static void recreatePages(final String student, final LocalDate date) { - // recreate the pages with a fresh sharedGraph so the graph is reset for the selected student/date - if (sharedGraph == null) { - sharedGraph = new JLineGraph(); - } else { - sharedGraph = new JLineGraph(); - } - - // Clear any previous listeners to avoid stale references - clearDateChangeListeners(); - clearStudentChangeListeners(); - - contentPanel.removeAll(); - contentPanel.add(Homepage.create(), "homepage"); - - // Instantiate pages into locals so we can register listeners if they implement the interface - Braille braille = new Braille(student, date, sharedGraph); - contentPanel.add(braille, "braille"); - if (braille instanceof DateChangeListener d) { - addDateChangeListener(d); - } - if (braille instanceof StudentChangeListener s) { - addStudentChangeListener(s); - } - - Abacus abacus = new Abacus(student, date, sharedGraph); - contentPanel.add(abacus, "abacus"); - if (abacus instanceof DateChangeListener d2) { - addDateChangeListener(d2); - } - if (abacus instanceof StudentChangeListener s2) { - addStudentChangeListener(s2); - } - - BrailleNote brailleNote = new BrailleNote(student, date, sharedGraph); - contentPanel.add(brailleNote, "braillenote"); - if (brailleNote instanceof DateChangeListener d3) { - addDateChangeListener(d3); - } - if (brailleNote instanceof StudentChangeListener s3) { - addStudentChangeListener(s3); - } - - DigitalLiteracy dl = new DigitalLiteracy(student, date, sharedGraph); - contentPanel.add(dl, "digitalliteracy"); - if (dl instanceof DateChangeListener d4) { - addDateChangeListener(d4); - } - if (dl instanceof StudentChangeListener s4) { - addStudentChangeListener(s4); - } - - // pages that don't currently need date-driven updates remain created inline - contentPanel.add(new BrailleSense(student, date, sharedGraph), "braillesense"); - contentPanel.add(new CVI(student, date, sharedGraph), "cvi"); - - IOS ios = new IOS(student, date, sharedGraph); - contentPanel.add(ios, "ios"); - if (ios instanceof DateChangeListener d5) { - addDateChangeListener(d5); - } - if (ios instanceof StudentChangeListener s5) { - addStudentChangeListener(s5); - } - - Keyboarding keyboarding = new Keyboarding(student, date, sharedGraph); - contentPanel.add(keyboarding, "keyboarding"); - if (keyboarding instanceof DateChangeListener d6) { - addDateChangeListener(d6); - } - if (keyboarding instanceof StudentChangeListener s6) { - addStudentChangeListener(s6); - } - - contentPanel.add(new Observations(student, date), "observations"); - - ScreenReader sr = new ScreenReader(student, date, sharedGraph); - contentPanel.add(sr, "screenreader"); - if (sr instanceof DateChangeListener d7) { - addDateChangeListener(d7); - } - if (sr instanceof StudentChangeListener s7) { - addStudentChangeListener(s7); - } - - contentPanel.add(new SessionNotes(student, date, sharedGraph), "sessionnotes"); - contentPanel.add(new ContactLog(student, date, sharedGraph), "contactlog"); - contentPanel.add(new InstructionalMaterials(), "instructionalmaterials"); - - contentPanel.revalidate(); - contentPanel.repaint(); - showPage("homepage", null); - } - - /** - * Show a page previously registered with the CardLayout. If a component - * is provided and not yet added it will be registered under the given name. - * - * @param name registration name for the page - * @param comp optional component instance to add (may be null) - */ - public static void showPage(final String name, final JComponent comp) { - CardLayout cl = (CardLayout) contentPanel.getLayout(); - if (comp != null && comp.getParent() == null) { - contentPanel.add(comp, name); - } - cl.show(contentPanel, name); - } - - /** - * Private constructor to prevent instantiation of this utility/entry class. - */ - private Main() { - throw new AssertionError("Not instantiable"); - } -} +package com.studentgui.app; + +import java.awt.BorderLayout; +import java.awt.CardLayout; +import java.awt.FlowLayout; +import java.time.LocalDate; +import java.time.format.DateTimeParseException; +import java.util.List; + +import javax.swing.JButton; +import javax.swing.JComboBox; +import javax.swing.JComponent; +import javax.swing.JFrame; +import javax.swing.JLabel; +import javax.swing.JPanel; +import javax.swing.JTextField; +import javax.swing.SwingUtilities; +import javax.swing.UIManager; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import com.studentgui.apphelpers.Helpers; +import com.studentgui.apphelpers.SqlGenerate; +import com.studentgui.apppages.Abacus; +import com.studentgui.apppages.Braille; +import com.studentgui.apppages.BrailleNote; +import com.studentgui.apppages.BrailleSense; +import com.studentgui.apppages.CVI; +import com.studentgui.apppages.ContactLog; +import com.studentgui.apppages.DigitalLiteracy; +import com.studentgui.apppages.Homepage; +import com.studentgui.apppages.IOS; +import com.studentgui.apppages.InstructionalMaterials; +import com.studentgui.apppages.JLineGraph; +import com.studentgui.apppages.Keyboarding; +import com.studentgui.apppages.Observations; +import com.studentgui.apppages.ScreenReader; +import com.studentgui.apppages.SessionNotes; +import com.studentgui.apptheming.Theme; + +/** + * Main application entry and UI wiring for the Student Skills Progressions app. + * + * This class builds the top-level window, menu, and registers the skill pages + * (each page is a JPanel). It's intentionally lightweight; most functionality + * for database access and page logic lives in helper classes and the page + * components under com.studentgui.apppages. + */ +/** + * Application bootstrap and top-level UI wiring. Builds the main JFrame, + * registers pages, and provides a small top control bar for switching + * students and pages. + */ +/** + * Application bootstrap and top-level UI wiring. Builds the main JFrame, + * registers pages, and provides a small top control bar for switching + * students and pages. + */ +/** + * Application entry point and top-level UI wiring for the Student Skills + * Progressions application. Builds the main frame, menu and registers per-page + * panels under a CardLayout. + */ +public class Main { + /** + * Bootstrap logging/system properties very early so Logback can resolve + * file locations and the per-run timestamp before any logger is + * initialized. This static block sets APP_HOME and LOG_TS and performs + * a cleanup of old log files older than 7 days. + */ + static { + try { + // Ensure Helpers.APP_HOME is initialized and use it for logging + String appHome = com.studentgui.apphelpers.Helpers.APP_HOME.toString(); + System.setProperty("APP_HOME", appHome); + // unix epoch seconds appended to per-run log filename + String ts = String.valueOf(java.time.Instant.now().getEpochSecond()); + System.setProperty("LOG_TS", ts); + + // create logs dir + java.nio.file.Path logs = java.nio.file.Paths.get(appHome).resolve("logs"); + java.nio.file.Files.createDirectories(logs); + + // Cleanup: remove log files older than 7 days (by last modified time) + long cutoff = java.time.Instant.now().minus(java.time.Duration.ofDays(7)).toEpochMilli(); + try (java.nio.file.DirectoryStream ds = java.nio.file.Files.newDirectoryStream(logs, "log_*.log")) { + for (java.nio.file.Path p : ds) { + try { + java.nio.file.attribute.FileTime ft = java.nio.file.Files.getLastModifiedTime(p); + if (ft.toMillis() < cutoff) { + java.nio.file.Files.deleteIfExists(p); + } + } catch (Exception ex) { + // Swallow cleanup exceptions; logging isn't available yet. + } + } + } catch (Exception ex) { + // ignore + } + // also remove consolidated data dump files older than retention + try (java.nio.file.DirectoryStream ds2 = java.nio.file.Files.newDirectoryStream(logs, "data_dumps_*.log")) { + for (java.nio.file.Path p : ds2) { + try { + java.nio.file.attribute.FileTime ft = java.nio.file.Files.getLastModifiedTime(p); + if (ft.toMillis() < cutoff) { + java.nio.file.Files.deleteIfExists(p); + } + } catch (Exception ex) { + // Swallow cleanup exceptions; logging isn't available yet. + } + } + } catch (Exception ex) { + // ignore + } + } catch (Exception ex) { + // If anything here fails, continue — logging may not be configured yet. + } + } + + private static final Logger LOG = LoggerFactory.getLogger(Main.class); + private static JFrame frame; + private static JPanel contentPanel; + private static JLineGraph sharedGraph; + /** + * Shared JLineGraph instance used across pages. + * + * Pages are constructed with the shared graph passed into their + * constructors (see recreatePages). The shared graph is registered + * with {@link #addSettingsChangeListener(SettingsChangeListener)} so + * it receives runtime preference updates. If a page creates its own + * page-local JLineGraph instance it should register it with + * {@link #addSettingsChangeListener(SettingsChangeListener)} and remove + * it when disposed to ensure it receives preference changes and to + * avoid leaking listeners. + */ + // current date used by the top bar (can be updated without recreating pages) + private static java.time.LocalDate currentDate; + private static String currentStudent; + // Listeners to notify when the top-bar date changes + private static final java.util.List dateListeners = new java.util.concurrent.CopyOnWriteArrayList<>(); + + /** + * Register a listener to be notified when the application date is changed via the top bar. + * + * @param l listener to register (ignored when null) + */ + public static void addDateChangeListener(final DateChangeListener l) { + if (l != null) { + dateListeners.add(l); + } + } + + /** + * Remove a previously registered date change listener. + * + * @param l listener to remove (ignored when null) + */ + public static void removeDateChangeListener(final DateChangeListener l) { + if (l != null) { + dateListeners.remove(l); + } + } + + /** + * Clear all registered date change listeners. + */ + public static void clearDateChangeListeners() { + dateListeners.clear(); + } + + /** + * Notify all registered date listeners that the application date has changed. + * + * @param d new application date + */ + private static void notifyDateChanged(final java.time.LocalDate d) { + for (DateChangeListener l : dateListeners) { + try { + l.dateChanged(d); + } catch (Exception ex) { + LOG.warn("DateChangeListener threw: {}", ex.toString()); + } + } + } + // Student change listeners + private static final java.util.List studentListeners = new java.util.concurrent.CopyOnWriteArrayList<>(); + /** + * Register a listener to be notified when the selected student is changed. + * + * @param l listener to register (ignored when null) + */ + public static void addStudentChangeListener(final StudentChangeListener l) { + if (l != null) { + studentListeners.add(l); + } + } + + /** + * Remove a previously registered student change listener. + * + * @param l listener to remove (ignored when null) + */ + public static void removeStudentChangeListener(final StudentChangeListener l) { + if (l != null) { + studentListeners.remove(l); + } + } + + /** + * Clear all registered student change listeners. + */ + public static void clearStudentChangeListeners() { + studentListeners.clear(); + } + + /** + * Notify registered student change listeners that the selected student has changed. + * + * @param s new selected student name + */ + private static void notifyStudentChanged(final String s) { + currentStudent = s; + for (StudentChangeListener l : studentListeners) { + try { + l.studentChanged(s); + } catch (Exception ex) { + LOG.warn("StudentChangeListener threw: {}", ex.toString()); + } + } + } + + + // Settings change listeners + private static final java.util.List settingsListeners = new java.util.concurrent.CopyOnWriteArrayList<>(); + + /** + * Register a listener to be notified when application settings change. + * Implementations should read values from {@link com.studentgui.apphelpers.Settings} + * when {@link SettingsChangeListener#settingsChanged()} is invoked. + * + * @param l listener to register (ignored when null) + */ + public static void addSettingsChangeListener(final SettingsChangeListener l) { + if (l != null) { + settingsListeners.add(l); + } + } + + /** + * Remove a previously registered settings change listener. + * + * @param l listener to remove (ignored when null) + */ + public static void removeSettingsChangeListener(final SettingsChangeListener l) { + if (l != null) { + settingsListeners.remove(l); + } + } + + /** + * Clear all registered settings change listeners. + */ + public static void clearSettingsChangeListeners() { + settingsListeners.clear(); + } + + /** + * Notify all registered settings listeners that application settings have been changed. + * This is typically invoked after persisting preferences through + * {@link com.studentgui.apphelpers.Settings}. + */ + public static void notifySettingsChanged() { + for (SettingsChangeListener l : settingsListeners) { + try { + l.settingsChanged(); + } catch (Exception ex) { + LOG.warn("SettingsChangeListener threw: {}", ex.toString()); + } + } + } + + /** + * Application entry point. Initializes helpers, database, and launches the + * Swing UI on the EDT. + * + * @param args command-line arguments (unused) + */ + public static void main(final String[] args) { + // Apply saved look and feel (default to light) + // Settings.get and setTheme handle any expected failures internally; + // call directly so we avoid a broad RuntimeException catch. + String saved = com.studentgui.apphelpers.Settings.get("theme", "light"); + setTheme(saved); + + // Initialize helpers and DB + Helpers.setStartDir(); + Helpers.createFolderHierarchy(); + SqlGenerate.initializeDatabase(); + + SwingUtilities.invokeLater(() -> { + frame = new JFrame("Student Skills Progressions"); + frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE); + frame.setSize(1000, 700); + frame.setLocationRelativeTo(null); + + // Menu bar: obtain the app menu bar from Theme, insert a File->Exit menu at the far left + javax.swing.JMenuBar themeBar = Theme.createMenuBar(); + if (themeBar == null) { + themeBar = new javax.swing.JMenuBar(); + } + javax.swing.JMenu fileMenu = new javax.swing.JMenu("File"); + javax.swing.JMenuItem exitItem = new javax.swing.JMenuItem("Exit"); + exitItem.addActionListener(e -> { + LOG.info("Exit requested via File->Exit"); + if (frame != null) { + frame.dispose(); + } + System.exit(0); + }); + fileMenu.add(exitItem); + // Insert file menu at position 0 so it appears on the far left + themeBar.add(fileMenu, 0); + // Ensure the Themes menu (if present) appears immediately after File + int themesIdx = -1; + for (int i = 0; i < themeBar.getMenuCount(); i++) { + javax.swing.JMenu m = themeBar.getMenu(i); + if (m != null && "Themes".equals(m.getText())) { themesIdx = i; break; } + } + if (themesIdx > 1) { + javax.swing.JMenu themesMenu = themeBar.getMenu(themesIdx); + themeBar.remove(themesIdx); + themeBar.add(themesMenu, 1); + } + frame.setJMenuBar(themeBar); + + + contentPanel = new JPanel(new CardLayout()); + frame.add(contentPanel, BorderLayout.CENTER); + + // Top control bar: student selector, date, and navigation + JPanel topBar = buildTopBar(); + frame.add(topBar, BorderLayout.NORTH); + + // Create initial shared graph and pages for the first student + sharedGraph = new JLineGraph(); + // Register shared graph to receive settings change notifications + addSettingsChangeListener(sharedGraph); + List students = Helpers.getStudents(); + String demoStudent = students.isEmpty() ? "Demo Student" : students.get(0); + LocalDate today = LocalDate.now(); + currentDate = today; + recreatePages(demoStudent, today); + + frame.setVisible(true); + }); + } + + /** + * Change application theme at runtime. Supported values: "light", "dark", "darcula". + * This method updates the installed Look and Feel and refreshes the main frame. + * + * @param theme human-friendly theme name or fully-qualified LookAndFeel class name + */ + public static void setTheme(final String theme) { + try { + String t = theme == null ? "light" : theme; + // Common keywords for bundled themes + switch (t.toLowerCase()) { + case "dark": + case "flatdarklaf": + UIManager.setLookAndFeel(new com.formdev.flatlaf.FlatDarkLaf()); + break; + case "darcula": + // Darcula-like: use FlatDarkLaf as fallback + UIManager.setLookAndFeel(new com.formdev.flatlaf.FlatDarkLaf()); + break; + case "light": + case "flatlightlaf": + UIManager.setLookAndFeel(new com.formdev.flatlaf.FlatLightLaf()); + break; + default: + // If the string looks like a fully-qualified class name, try to set it directly. + if (t.contains(".")) { + try { + UIManager.setLookAndFeel(t); + } catch (ReflectiveOperationException | javax.swing.UnsupportedLookAndFeelException ex) { + // Try to instantiate via reflection + try { + Class c = Class.forName(t); + Object o = c.getDeclaredConstructor().newInstance(); + if (o instanceof javax.swing.LookAndFeel) { + UIManager.setLookAndFeel((javax.swing.LookAndFeel) o); + } else { + // fallback to light + UIManager.setLookAndFeel(new com.formdev.flatlaf.FlatLightLaf()); + } + } catch (ReflectiveOperationException | javax.swing.UnsupportedLookAndFeelException ex2) { + LOG.error("Failed to set look and feel by class name {}", t, ex2); + UIManager.setLookAndFeel(new com.formdev.flatlaf.FlatLightLaf()); + } + } + } else { + // Try to find an installed LAF by name + boolean applied = false; + for (UIManager.LookAndFeelInfo info : UIManager.getInstalledLookAndFeels()) { + if (info.getName().equalsIgnoreCase(t) || info.getName().toLowerCase().contains(t.toLowerCase())) { + UIManager.setLookAndFeel(info.getClassName()); + applied = true; + break; + } + } + if (!applied) { + // default fallback + UIManager.setLookAndFeel(new com.formdev.flatlaf.FlatLightLaf()); + } + } + break; + } + if (frame != null) { + javax.swing.SwingUtilities.updateComponentTreeUI(frame); + frame.pack(); + } + } catch (ReflectiveOperationException | javax.swing.UnsupportedLookAndFeelException | IllegalArgumentException e) { + LOG.error("Failed to set theme {}", theme, e); + } + } + + private static JPanel buildTopBar() { + JPanel bar = new JPanel(new FlowLayout(FlowLayout.LEFT)); + List students = Helpers.getStudents(); + JComboBox studentBox = new JComboBox<>(students.toArray(new String[0])); + studentBox.setEditable(false); + + JLabel dateLabel = new JLabel("Date (YYYY-MM-DD):"); + JTextField dateField = new JTextField(LocalDate.now().toString(), 10); + + JButton goBtn = new JButton("Apply"); + + bar.add(new JLabel("Student:")); + bar.add(studentBox); + bar.add(dateLabel); + bar.add(dateField); + bar.add(goBtn); + + goBtn.addActionListener(e -> { + String selected = (String) studentBox.getSelectedItem(); + LocalDate date = LocalDate.now(); + try { + date = LocalDate.parse(dateField.getText()); + } catch (DateTimeParseException ex) { + // keep today + } + // Update the app's current date and selected student without recreating pages; show a confirmation dialog. + currentDate = date; + currentStudent = selected; + javax.swing.JOptionPane.showMessageDialog(frame, + "The date has been updated to " + date.toString(), + "Date Updated", + javax.swing.JOptionPane.INFORMATION_MESSAGE); + // Notify registered pages so they can update any internal state + notifyDateChanged(date); + notifyStudentChanged(selected); + }); + + // Navigation buttons removed from top bar per UI request; pages can still be selected via menu + + return bar; + } + + /** + * Recreate per-page panels for the provided student and date. This replaces + * the CardLayout content so the shared graph and pages are reset. + */ + /** + * Recreate per-page panels for the provided student and date. This + * replaces the CardLayout content so the shared graph and pages are reset. + * + * @param student selected student's display name + * @param date the session date for newly created pages + */ + private static void recreatePages(final String student, final LocalDate date) { + // recreate the pages with a fresh sharedGraph so the graph is reset for the selected student/date + if (sharedGraph == null) { + sharedGraph = new JLineGraph(); + } else { + sharedGraph = new JLineGraph(); + } + + // Clear any previous listeners to avoid stale references + clearDateChangeListeners(); + clearStudentChangeListeners(); + + contentPanel.removeAll(); + contentPanel.add(Homepage.create(), "homepage"); + + // Instantiate pages into locals so we can register listeners if they implement the interface + Braille braille = new Braille(student, date, sharedGraph); + contentPanel.add(braille, "braille"); + if (braille instanceof DateChangeListener d) { + addDateChangeListener(d); + } + if (braille instanceof StudentChangeListener s) { + addStudentChangeListener(s); + } + + Abacus abacus = new Abacus(student, date, sharedGraph); + contentPanel.add(abacus, "abacus"); + if (abacus instanceof DateChangeListener d2) { + addDateChangeListener(d2); + } + if (abacus instanceof StudentChangeListener s2) { + addStudentChangeListener(s2); + } + + BrailleNote brailleNote = new BrailleNote(student, date, sharedGraph); + contentPanel.add(brailleNote, "braillenote"); + if (brailleNote instanceof DateChangeListener d3) { + addDateChangeListener(d3); + } + if (brailleNote instanceof StudentChangeListener s3) { + addStudentChangeListener(s3); + } + + DigitalLiteracy dl = new DigitalLiteracy(student, date, sharedGraph); + contentPanel.add(dl, "digitalliteracy"); + if (dl instanceof DateChangeListener d4) { + addDateChangeListener(d4); + } + if (dl instanceof StudentChangeListener s4) { + addStudentChangeListener(s4); + } + + // pages that don't currently need date-driven updates remain created inline + contentPanel.add(new BrailleSense(student, date, sharedGraph), "braillesense"); + contentPanel.add(new CVI(student, date, sharedGraph), "cvi"); + + IOS ios = new IOS(student, date, sharedGraph); + contentPanel.add(ios, "ios"); + if (ios instanceof DateChangeListener d5) { + addDateChangeListener(d5); + } + if (ios instanceof StudentChangeListener s5) { + addStudentChangeListener(s5); + } + + Keyboarding keyboarding = new Keyboarding(student, date, sharedGraph); + contentPanel.add(keyboarding, "keyboarding"); + if (keyboarding instanceof DateChangeListener d6) { + addDateChangeListener(d6); + } + if (keyboarding instanceof StudentChangeListener s6) { + addStudentChangeListener(s6); + } + + contentPanel.add(new Observations(student, date), "observations"); + + ScreenReader sr = new ScreenReader(student, date, sharedGraph); + contentPanel.add(sr, "screenreader"); + if (sr instanceof DateChangeListener d7) { + addDateChangeListener(d7); + } + if (sr instanceof StudentChangeListener s7) { + addStudentChangeListener(s7); + } + + contentPanel.add(new SessionNotes(student, date, sharedGraph), "sessionnotes"); + contentPanel.add(new ContactLog(student, date, sharedGraph), "contactlog"); + contentPanel.add(new InstructionalMaterials(), "instructionalmaterials"); + + contentPanel.revalidate(); + contentPanel.repaint(); + showPage("homepage", null); + } + + /** + * Show a page previously registered with the CardLayout. If a component + * is provided and not yet added it will be registered under the given name. + * + * @param name registration name for the page + * @param comp optional component instance to add (may be null) + */ + public static void showPage(final String name, final JComponent comp) { + CardLayout cl = (CardLayout) contentPanel.getLayout(); + if (comp != null && comp.getParent() == null) { + contentPanel.add(comp, name); + } + cl.show(contentPanel, name); + } + + /** + * Private constructor to prevent instantiation of this utility/entry class. + */ + private Main() { + throw new AssertionError("Not instantiable"); + } +} diff --git a/src/main/java/com/studentgui/app/PreferencesDialog.java b/src/main/java/com/studentgui/app/PreferencesDialog.java index a8485fb..3004f7f 100644 --- a/src/main/java/com/studentgui/app/PreferencesDialog.java +++ b/src/main/java/com/studentgui/app/PreferencesDialog.java @@ -1,77 +1,77 @@ -package com.studentgui.app; - -import java.awt.BorderLayout; -import java.awt.FlowLayout; -import java.awt.Frame; - -import javax.swing.JButton; -import javax.swing.JCheckBox; -import javax.swing.JDialog; -import javax.swing.JLabel; -import javax.swing.JPanel; -import javax.swing.JTextField; - -import com.studentgui.apphelpers.Settings; - -/** - * Simple modal preferences dialog exposing a few runtime toggles that - * affect chart rendering. Preferences are persisted via - * {@link com.studentgui.apphelpers.Settings} and listeners are notified - * through {@link Main#notifySettingsChanged()}. - */ -public final class PreferencesDialog { - private PreferencesDialog() { throw new AssertionError(); } - - /** - * Show the modal preferences dialog. The dialog persists changes to - * {@link com.studentgui.apphelpers.Settings} and notifies runtime - * listeners via {@link Main#notifySettingsChanged()}. - * - * @param owner optional parent frame for dialog positioning - */ - public static void showDialog(final Frame owner) { - final JDialog dlg = new JDialog(owner, "Preferences", true); - dlg.setLayout(new BorderLayout()); - - JPanel center = new JPanel(new FlowLayout(FlowLayout.LEFT)); - boolean jitterEnabled = Boolean.parseBoolean(Settings.get("jitter.enabled", "true")); - boolean deterministic = Boolean.parseBoolean(Settings.get("jitter.deterministic", "false")); - String seed = Settings.get("jitter.seed", ""); - boolean dumpsEnabled = Boolean.parseBoolean(Settings.get("dump.enabled", "false")); - - final JCheckBox jitterCb = new JCheckBox("Enable jitter", jitterEnabled); - final JCheckBox detCb = new JCheckBox("Deterministic (seeded)", deterministic); - final JTextField seedField = new JTextField(seed == null ? "" : seed, 12); - final JCheckBox dumpsCb = new JCheckBox("Enable per-page data dumps", dumpsEnabled); - - center.add(jitterCb); - center.add(detCb); - center.add(dumpsCb); - center.add(new JLabel("Seed:")); - center.add(seedField); - - JPanel south = new JPanel(new FlowLayout(FlowLayout.RIGHT)); - JButton save = new JButton("Save"); - JButton cancel = new JButton("Cancel"); - - save.addActionListener(e -> { - Settings.put("jitter.enabled", String.valueOf(jitterCb.isSelected())); - Settings.put("jitter.deterministic", String.valueOf(detCb.isSelected())); - Settings.put("jitter.seed", seedField.getText().trim()); - Settings.put("dump.enabled", String.valueOf(dumpsCb.isSelected())); - // notify runtime listeners - Main.notifySettingsChanged(); - dlg.dispose(); - }); - - cancel.addActionListener(e -> dlg.dispose()); - south.add(cancel); - south.add(save); - - dlg.add(center, BorderLayout.CENTER); - dlg.add(south, BorderLayout.SOUTH); - dlg.pack(); - dlg.setLocationRelativeTo(owner); - dlg.setVisible(true); - } -} +package com.studentgui.app; + +import java.awt.BorderLayout; +import java.awt.FlowLayout; +import java.awt.Frame; + +import javax.swing.JButton; +import javax.swing.JCheckBox; +import javax.swing.JDialog; +import javax.swing.JLabel; +import javax.swing.JPanel; +import javax.swing.JTextField; + +import com.studentgui.apphelpers.Settings; + +/** + * Simple modal preferences dialog exposing a few runtime toggles that + * affect chart rendering. Preferences are persisted via + * {@link com.studentgui.apphelpers.Settings} and listeners are notified + * through {@link Main#notifySettingsChanged()}. + */ +public final class PreferencesDialog { + private PreferencesDialog() { throw new AssertionError(); } + + /** + * Show the modal preferences dialog. The dialog persists changes to + * {@link com.studentgui.apphelpers.Settings} and notifies runtime + * listeners via {@link Main#notifySettingsChanged()}. + * + * @param owner optional parent frame for dialog positioning + */ + public static void showDialog(final Frame owner) { + final JDialog dlg = new JDialog(owner, "Preferences", true); + dlg.setLayout(new BorderLayout()); + + JPanel center = new JPanel(new FlowLayout(FlowLayout.LEFT)); + boolean jitterEnabled = Boolean.parseBoolean(Settings.get("jitter.enabled", "true")); + boolean deterministic = Boolean.parseBoolean(Settings.get("jitter.deterministic", "false")); + String seed = Settings.get("jitter.seed", ""); + boolean dumpsEnabled = Boolean.parseBoolean(Settings.get("dump.enabled", "false")); + + final JCheckBox jitterCb = new JCheckBox("Enable jitter", jitterEnabled); + final JCheckBox detCb = new JCheckBox("Deterministic (seeded)", deterministic); + final JTextField seedField = new JTextField(seed == null ? "" : seed, 12); + final JCheckBox dumpsCb = new JCheckBox("Enable per-page data dumps", dumpsEnabled); + + center.add(jitterCb); + center.add(detCb); + center.add(dumpsCb); + center.add(new JLabel("Seed:")); + center.add(seedField); + + JPanel south = new JPanel(new FlowLayout(FlowLayout.RIGHT)); + JButton save = new JButton("Save"); + JButton cancel = new JButton("Cancel"); + + save.addActionListener(e -> { + Settings.put("jitter.enabled", String.valueOf(jitterCb.isSelected())); + Settings.put("jitter.deterministic", String.valueOf(detCb.isSelected())); + Settings.put("jitter.seed", seedField.getText().trim()); + Settings.put("dump.enabled", String.valueOf(dumpsCb.isSelected())); + // notify runtime listeners + Main.notifySettingsChanged(); + dlg.dispose(); + }); + + cancel.addActionListener(e -> dlg.dispose()); + south.add(cancel); + south.add(save); + + dlg.add(center, BorderLayout.CENTER); + dlg.add(south, BorderLayout.SOUTH); + dlg.pack(); + dlg.setLocationRelativeTo(owner); + dlg.setVisible(true); + } +} diff --git a/src/main/java/com/studentgui/app/SettingsChangeListener.java b/src/main/java/com/studentgui/app/SettingsChangeListener.java index fe5b68d..1ecbdcf 100644 --- a/src/main/java/com/studentgui/app/SettingsChangeListener.java +++ b/src/main/java/com/studentgui/app/SettingsChangeListener.java @@ -1,13 +1,13 @@ -package com.studentgui.app; - -/** - * Simple listener interface for application-wide settings changes. - */ -public interface SettingsChangeListener { - /** - * Invoked when application settings have been changed and persisted. - * Implementations should read the desired values from the Settings - * helper and update any runtime state accordingly. - */ - void settingsChanged(); -} +package com.studentgui.app; + +/** + * Simple listener interface for application-wide settings changes. + */ +public interface SettingsChangeListener { + /** + * Invoked when application settings have been changed and persisted. + * Implementations should read the desired values from the Settings + * helper and update any runtime state accordingly. + */ + void settingsChanged(); +} diff --git a/src/main/java/com/studentgui/app/StudentChangeListener.java b/src/main/java/com/studentgui/app/StudentChangeListener.java index 3f60aad..cfd4f61 100644 --- a/src/main/java/com/studentgui/app/StudentChangeListener.java +++ b/src/main/java/com/studentgui/app/StudentChangeListener.java @@ -1,12 +1,12 @@ -package com.studentgui.app; - -/** - * Listener for application-wide student selection changes. - */ -public interface StudentChangeListener { - /** - * Called when the application selected student has changed. - * @param newStudent the newly selected student's display name (may be null) - */ - void studentChanged(String newStudent); -} +package com.studentgui.app; + +/** + * Listener for application-wide student selection changes. + */ +public interface StudentChangeListener { + /** + * Called when the application selected student has changed. + * @param newStudent the newly selected student's display name (may be null) + */ + void studentChanged(String newStudent); +} diff --git a/src/main/java/com/studentgui/apphelpers/Database.java b/src/main/java/com/studentgui/apphelpers/Database.java index 9a98489..f35ba61 100644 --- a/src/main/java/com/studentgui/apphelpers/Database.java +++ b/src/main/java/com/studentgui/apphelpers/Database.java @@ -1,565 +1,565 @@ -package com.studentgui.apphelpers; - -import java.sql.Connection; -import java.sql.DriverManager; -import java.sql.PreparedStatement; -import java.sql.ResultSet; -import java.sql.SQLException; -import java.sql.Statement; -import java.time.LocalDate; -import java.util.ArrayList; -import java.util.HashMap; -import java.util.List; -import java.util.Map; - -/** - * Centralized database helper for the normalized SQLite schema. - * - *

Provides convenience methods to get-or-create Students and ProgressTypes, - * create ProgressSessions, ensure AssessmentParts, insert/fetch assessment - * results, and save session-specific notes. Use these helpers instead of - * running per-page DDL throughout the codebase.

- */ -public class Database { - - /** - * Private constructor to prevent instantiation of this utility class. - */ - private Database() { - throw new AssertionError("Database is a utility class"); - } - - /** - * Obtain a new JDBC Connection to the application SQLite database. - * Caller is responsible for closing the connection (try-with-resources is recommended). - * - * @return new Connection - * @throws SQLException if the driver cannot open the database - */ - private static Connection getConnection() throws SQLException { - String url = "jdbc:sqlite:" + Helpers.DATABASE_PATH.toString(); - return DriverManager.getConnection(url); - } - - /** - * Get a student id by name, creating a new Student row when none exists. - * - * @param name student display name - * @return id of the existing or newly created student - * @throws SQLException on database errors - */ - public static int getOrCreateStudent(final String name) throws SQLException { - try (Connection c = getConnection()) { - try (PreparedStatement ps = c.prepareStatement("SELECT id FROM Student WHERE name = ?")) { - ps.setString(1, name); - try (ResultSet rs = ps.executeQuery()) { - if (rs.next()) { - return rs.getInt(1); - } - } - } - try (PreparedStatement ps = c.prepareStatement("INSERT INTO Student(name) VALUES (?)", Statement.RETURN_GENERATED_KEYS)) { - ps.setString(1, name); - ps.executeUpdate(); - try (ResultSet keys = ps.getGeneratedKeys()) { - if (keys.next()) { - return keys.getInt(1); - } - } - } - } - throw new SQLException("Failed to create or retrieve student"); - } - - /** - * Get or create a ProgressType row by name. - * - * @param name progress type display name - * @return database id of the progress type - * @throws SQLException on database errors - */ - public static int getOrCreateProgressType(final String name) throws SQLException { - try (Connection c = getConnection()) { - try (PreparedStatement ps = c.prepareStatement("SELECT id FROM ProgressType WHERE name = ?")) { - ps.setString(1, name); - try (ResultSet rs = ps.executeQuery()) { - if (rs.next()) { - return rs.getInt(1); - } - } - } - try (PreparedStatement ps = c.prepareStatement("INSERT INTO ProgressType(name) VALUES (?)", Statement.RETURN_GENERATED_KEYS)) { - ps.setString(1, name); - ps.executeUpdate(); - try (ResultSet keys = ps.getGeneratedKeys()) { - if (keys.next()) { - return keys.getInt(1); - } - } - } - } - throw new SQLException("Failed to create or retrieve ProgressType"); - } - - /** - * Ensure AssessmentPart rows exist for the given progress type. This uses - * SQL "INSERT OR IGNORE" so existing parts are preserved. - * - * @param progressTypeId id of the ProgressType - * @param codes array of part codes to ensure - * @throws SQLException on database errors - */ - public static void ensureAssessmentParts(final int progressTypeId, final String[] codes) throws SQLException { - try (Connection c = getConnection()) { - try (PreparedStatement ps = c.prepareStatement("INSERT OR IGNORE INTO AssessmentPart(progress_type_id, code, description) VALUES (?, ?, NULL)")) { - for (String code : codes) { - ps.setInt(1, progressTypeId); - ps.setString(2, code); - ps.addBatch(); - } - ps.executeBatch(); - } - } - } - - /** - * Remove any AssessmentPart rows for the given progress type whose code is - * not present in the provided canonical codes array. This helps clean up - * legacy/malformed entries that could cause part ordering mismatches. - * - * @param progressTypeId id of the ProgressType - * @param allowedCodes canonical set of codes to keep - * @throws SQLException on database errors - */ - public static void cleanupAssessmentParts(final int progressTypeId, final String[] allowedCodes) throws SQLException { - if (allowedCodes == null || allowedCodes.length == 0) { - return; - } - try (Connection c = getConnection()) { - StringBuilder sb = new StringBuilder(); - for (int i = 0; i < allowedCodes.length; i++) { - if (i > 0) { sb.append(','); } - sb.append('?'); - } - String sql = "DELETE FROM AssessmentPart WHERE progress_type_id = ? AND code NOT IN (" + sb.toString() + ")"; - try (PreparedStatement ps = c.prepareStatement(sql)) { - ps.setInt(1, progressTypeId); - for (int i = 0; i < allowedCodes.length; i++) { - ps.setString(i + 2, allowedCodes[i]); - } - ps.executeUpdate(); - } - } - } - - /** - * Create a ProgressSession for a student and progress type on the given date. - * - * @param studentId existing student id - * @param progressTypeId existing progress type id - * @param date session date - * @return generated ProgressSession id - * @throws SQLException on database errors - */ - public static int createProgressSession(final int studentId, final int progressTypeId, final LocalDate date) throws SQLException { - try (Connection c = getConnection()) { - try (PreparedStatement ps = c.prepareStatement("INSERT INTO ProgressSession(student_id, progress_type_id, date, notes) VALUES (?, ?, ?, NULL)", Statement.RETURN_GENERATED_KEYS)) { - ps.setInt(1, studentId); - ps.setInt(2, progressTypeId); - ps.setString(3, date.toString()); - ps.executeUpdate(); - try (ResultSet keys = ps.getGeneratedKeys()) { - if (keys.next()) { return keys.getInt(1); } - } - } - } - throw new SQLException("Failed to create ProgressSession"); - } - - /** - * Insert assessment results for a session. The {@code codes} and {@code scores} - * arrays must be parallel and correspond to existing AssessmentPart codes. - * Unknown part codes are ignored. - * - * @param sessionId progress session id - * @param progressTypeId progress type id - * @param codes array of part codes - * @param scores array of integer scores - * @throws SQLException on database errors - */ - public static void insertAssessmentResults(final int sessionId, final int progressTypeId, final String[] codes, final int[] scores) throws SQLException { - if (codes.length != scores.length) { throw new IllegalArgumentException("codes and scores length mismatch"); } - try (Connection c = getConnection()) { - // cache part ids - Map partIdMap = new HashMap<>(); - try (PreparedStatement ps = c.prepareStatement("SELECT id, code FROM AssessmentPart WHERE progress_type_id = ?")) { - ps.setInt(1, progressTypeId); - try (ResultSet rs = ps.executeQuery()) { - while (rs.next()) { - partIdMap.put(rs.getString("code"), rs.getInt("id")); - } - } - } - try (PreparedStatement ins = c.prepareStatement("INSERT INTO AssessmentResult(session_id, part_id, score) VALUES (?, ?, ?)") ) { - for (int i = 0; i < codes.length; i++) { - Integer partId = partIdMap.get(codes[i]); - if (partId == null) { - // skip unknown part - continue; - } - ins.setInt(1, sessionId); - ins.setInt(2, partId); - ins.setInt(3, scores[i]); - ins.addBatch(); - } - ins.executeBatch(); - } - } - } - - /** - * Fetch the latest assessment result rows for a named student and progress type. - * Each returned row is a list of integer scores for the parts in canonical - * part order. - * - * @param studentName student display name - * @param progressTypeName progress type display name - * @param limit maximum number of recent sessions to fetch - * @return list of rows, each row is a list of integer scores - * @throws SQLException on database errors - */ - public static List> fetchLatestAssessmentResults(final String studentName, final String progressTypeName, final int limit) throws SQLException { - List> result = new ArrayList<>(); - try (Connection c = getConnection()) { - Integer studentId = null; - try (PreparedStatement ps = c.prepareStatement("SELECT id FROM Student WHERE name = ?")) { - ps.setString(1, studentName); - try (ResultSet rs = ps.executeQuery()) { - if (rs.next()) { studentId = rs.getInt(1); } - } - } - if (studentId == null) { return result; } - - Integer progressTypeId = null; - try (PreparedStatement ps = c.prepareStatement("SELECT id FROM ProgressType WHERE name = ?")) { - ps.setString(1, progressTypeName); - try (ResultSet rs = ps.executeQuery()) { - if (rs.next()) { progressTypeId = rs.getInt(1); } - } - } - if (progressTypeId == null) { return result; } - - // get parts in canonical order (by id) - List partIds = new ArrayList<>(); - try (PreparedStatement ps = c.prepareStatement("SELECT id, code FROM AssessmentPart WHERE progress_type_id = ? ORDER BY id ASC")) { - ps.setInt(1, progressTypeId); - try (ResultSet rs = ps.executeQuery()) { - while (rs.next()) { partIds.add(rs.getInt("id")); } - } - } - - // get latest session ids for this student and progress type - List sessionIds = new ArrayList<>(); - try (PreparedStatement ps = c.prepareStatement("SELECT id FROM ProgressSession WHERE student_id = ? AND progress_type_id = ? ORDER BY id DESC LIMIT ?")) { - ps.setInt(1, studentId); - ps.setInt(2, progressTypeId); - ps.setInt(3, limit); - try (ResultSet rs = ps.executeQuery()) { - while (rs.next()) { sessionIds.add(rs.getInt(1)); } - } - } - - // For each session, fetch scores mapped to parts - for (Integer sid : sessionIds) { - Map scoreByPart = new HashMap<>(); - try (PreparedStatement ps = c.prepareStatement("SELECT part_id, score FROM AssessmentResult WHERE session_id = ?")) { - ps.setInt(1, sid); - try (ResultSet rs = ps.executeQuery()) { - while (rs.next()) { - scoreByPart.put(rs.getInt("part_id"), rs.getInt("score")); - } - } - } - List row = new ArrayList<>(); - for (Integer pid : partIds) { - Integer s = scoreByPart.get(pid); - row.add(s == null ? 0 : s); - } - result.add(row); - } - } - return result; - } - - /** - * Simple, immutable holder for time-series assessment results. - * - *

Contains a chronologically ordered list of session {@code dates} - * and a parallel list of integer score rows. Each entry in {@code rows} - * corresponds to the parts for a progress type in canonical order. - */ - public static class ResultsWithDates { - /** - * Ordered session dates (oldest first). Can be empty when no sessions exist. - */ - public final java.util.List dates; - - /** - * Parallel rows of integer scores. Each inner list corresponds to the - * assessment parts for a single session in canonical part order. May be - * empty when there are no sessions. - */ - public final java.util.List> rows; - - /** - * Create a ResultsWithDates instance. - * - * @param dates ordered session dates (oldest-first) - * @param rows parallel list of score rows matching {@code dates} - */ - public ResultsWithDates(java.util.List dates, java.util.List> rows) { - this.dates = dates; - this.rows = rows; - } - } - - /** - * Fetch the latest assessment rows along with their session dates. - * Rows and dates are ordered oldest-first to facilitate time series plotting. - * - * @param studentName student display name to filter results for - * @param progressTypeName progress type display name (e.g., "Braille") - * @param limit maximum number of recent sessions to return - * @return ResultsWithDates holding an ordered list of session dates and parallel rows of scores - * @throws SQLException on database errors - */ - public static ResultsWithDates fetchLatestAssessmentResultsWithDates(final String studentName, final String progressTypeName, final int limit) throws SQLException { - java.util.List> result = new ArrayList<>(); - java.util.List dates = new ArrayList<>(); - try (Connection c = getConnection()) { - Integer studentId = null; - try (PreparedStatement ps = c.prepareStatement("SELECT id FROM Student WHERE name = ?")) { - ps.setString(1, studentName); - try (ResultSet rs = ps.executeQuery()) { - if (rs.next()) { - studentId = rs.getInt(1); - } - } - } - if (studentId == null) { return new ResultsWithDates(dates, result); } - - Integer progressTypeId = null; - try (PreparedStatement ps = c.prepareStatement("SELECT id FROM ProgressType WHERE name = ?")) { - ps.setString(1, progressTypeName); - try (ResultSet rs = ps.executeQuery()) { - if (rs.next()) { - progressTypeId = rs.getInt(1); - } - } - } - if (progressTypeId == null) { - return new ResultsWithDates(dates, result); - } - - // get parts in canonical order (by id) - java.util.List partIds = new ArrayList<>(); - try (PreparedStatement ps = c.prepareStatement("SELECT id, code FROM AssessmentPart WHERE progress_type_id = ? ORDER BY id ASC")) { - ps.setInt(1, progressTypeId); - try (ResultSet rs = ps.executeQuery()) { - while (rs.next()) { - partIds.add(rs.getInt("id")); - } - } - } - - // get latest session ids and dates for this student and progress type - java.util.List sessionIds = new ArrayList<>(); - java.util.List sessionDates = new ArrayList<>(); - try (PreparedStatement ps = c.prepareStatement("SELECT id, date FROM ProgressSession WHERE student_id = ? AND progress_type_id = ? ORDER BY id DESC LIMIT ?")) { - ps.setInt(1, studentId); ps.setInt(2, progressTypeId); ps.setInt(3, limit); - try (ResultSet rs = ps.executeQuery()) { - while (rs.next()) { - sessionIds.add(rs.getInt("id")); - sessionDates.add(java.time.LocalDate.parse(rs.getString("date"))); - } - } - } - - // We want chronological order (oldest first) - java.util.Collections.reverse(sessionIds); - java.util.Collections.reverse(sessionDates); - - // For each session, fetch scores mapped to parts and append row - for (Integer sid : sessionIds) { - Map scoreByPart = new HashMap<>(); - try (PreparedStatement ps = c.prepareStatement("SELECT part_id, score FROM AssessmentResult WHERE session_id = ?")) { - ps.setInt(1, sid); - try (ResultSet rs = ps.executeQuery()) { - while (rs.next()) { - scoreByPart.put(rs.getInt("part_id"), rs.getInt("score")); - } - } - } - java.util.List row = new ArrayList<>(); - for (Integer pid : partIds) { - Integer s = scoreByPart.get(pid); - row.add(s == null ? 0 : s); - } - result.add(row); - } - dates.addAll(sessionDates); - } - return new ResultsWithDates(dates, result); - } - - /** - * Insert a keyboarding-specific result linked to a ProgressSession. - * - * @param sessionId existing session id - * @param program program or curriculum name - * @param topic topic or lesson name - * @param speed words-per-minute - * @param accuracy accuracy percent - * @throws SQLException on database errors - */ - public static void insertKeyboardingResult(final int sessionId, final String program, final String topic, final int speed, final int accuracy) throws SQLException { - try (Connection c = getConnection()) { - try (PreparedStatement ps = c.prepareStatement("INSERT INTO KeyboardingResult(session_id, program, topic, speed, accuracy) VALUES (?, ?, ?, ?, ?)")) { - ps.setInt(1, sessionId); - ps.setString(2, program); - ps.setString(3, topic); - ps.setInt(4, speed); - ps.setInt(5, accuracy); - ps.executeUpdate(); - } - } - } - - /** - * Save free-form notes for a given ProgressSession. - * - * @param sessionId progress session id - * @param notes free-form notes text - * @throws SQLException on database errors - */ - public static void saveSessionNotes(final int sessionId, final String notes) throws SQLException { - try (Connection c = getConnection()) { - try (PreparedStatement ps = c.prepareStatement("UPDATE ProgressSession SET notes = ? WHERE id = ?")) { - ps.setString(1, notes); - ps.setInt(2, sessionId); - ps.executeUpdate(); - } - } - } - - /** - * Save structured contact log details for a given ProgressSession. This - * will insert or replace a single ContactLog row tied to the session. - * - * @param sessionId existing session id - * @param studentName student display name - * @param date session date as text - * @param guardianName guardian or parent name - * @param contactMethod method of contact (phone/email/etc) - * @param phoneNumber phone number string - * @param emailAddress email address string - * @param contactResponse short description of response - * @param contactGeneral general contact summary - * @param contactSpecific specific items discussed - * @param contactNotes free-form notes - * @throws SQLException on database errors - */ - public static void saveContactLog(final int sessionId, final String studentName, final String date, final String guardianName, final String contactMethod, final String phoneNumber, final String emailAddress, final String contactResponse, final String contactGeneral, final String contactSpecific, final String contactNotes) throws SQLException { - try (Connection c = getConnection()) { - try (PreparedStatement ps = c.prepareStatement("INSERT OR REPLACE INTO ContactLog(session_id, student_name, date, guardian_name, contact_method, phone_number, email_address, contact_response, contact_general, contact_specific, contact_notes) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)") ) { - ps.setInt(1, sessionId); - ps.setString(2, studentName); - ps.setString(3, date); - ps.setString(4, guardianName); - ps.setString(5, contactMethod); - ps.setString(6, phoneNumber); - ps.setString(7, emailAddress); - ps.setString(8, contactResponse); - ps.setString(9, contactGeneral); - ps.setString(10, contactSpecific); - ps.setString(11, contactNotes); - ps.executeUpdate(); - } - } - } - - /** - * Fetch the most recent ContactLog entry for the given student name. - * Returns a map of column names to string values, or null if none found. - * - * @param studentName student display name to search for - * @return map of contact log columns to values or null when not found - * @throws SQLException on database errors - */ - public static com.studentgui.apphelpers.dto.ContactPayload fetchLatestContactLog(final String studentName) throws SQLException { - try (Connection c = getConnection()) { - Integer studentId = null; - try (PreparedStatement ps = c.prepareStatement("SELECT id FROM Student WHERE name = ?")) { - ps.setString(1, studentName); - try (ResultSet rs = ps.executeQuery()) { - if (rs.next()) { - studentId = rs.getInt(1); - } - } - } - if (studentId == null) { - return null; - } - - // Find the latest session id for ProgressType 'ContactLog' - Integer ptId = null; - try (PreparedStatement ps = c.prepareStatement("SELECT id FROM ProgressType WHERE name = ?")) { - ps.setString(1, "ContactLog"); - try (ResultSet rs = ps.executeQuery()) { - if (rs.next()) { - ptId = rs.getInt(1); - } - } - } - if (ptId == null) { - return null; - } - - Integer sessionId = null; - try (PreparedStatement ps = c.prepareStatement("SELECT id FROM ProgressSession WHERE student_id = ? AND progress_type_id = ? ORDER BY id DESC LIMIT 1")) { - ps.setInt(1, studentId); - ps.setInt(2, ptId); - try (ResultSet rs = ps.executeQuery()) { - if (rs.next()) { - sessionId = rs.getInt(1); - } - } - } - if (sessionId == null) { - return null; - } - - try (PreparedStatement ps = c.prepareStatement("SELECT student_name, date, guardian_name, contact_method, phone_number, email_address, contact_response, contact_general, contact_specific, contact_notes FROM ContactLog WHERE session_id = ? ORDER BY id DESC LIMIT 1")) { - ps.setInt(1, sessionId); - try (ResultSet rs = ps.executeQuery()) { - if (rs.next()) { - com.studentgui.apphelpers.dto.ContactPayload p = new com.studentgui.apphelpers.dto.ContactPayload( - sessionId, - rs.getString("guardian_name"), - rs.getString("contact_method"), - rs.getString("phone_number"), - rs.getString("email_address"), - rs.getString("contact_response"), - rs.getString("contact_general"), - rs.getString("contact_specific"), - rs.getString("contact_notes") - ); - return p; - } - } - } - return null; - } - } - -} +package com.studentgui.apphelpers; + +import java.sql.Connection; +import java.sql.DriverManager; +import java.sql.PreparedStatement; +import java.sql.ResultSet; +import java.sql.SQLException; +import java.sql.Statement; +import java.time.LocalDate; +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +/** + * Centralized database helper for the normalized SQLite schema. + * + *

Provides convenience methods to get-or-create Students and ProgressTypes, + * create ProgressSessions, ensure AssessmentParts, insert/fetch assessment + * results, and save session-specific notes. Use these helpers instead of + * running per-page DDL throughout the codebase.

+ */ +public class Database { + + /** + * Private constructor to prevent instantiation of this utility class. + */ + private Database() { + throw new AssertionError("Database is a utility class"); + } + + /** + * Obtain a new JDBC Connection to the application SQLite database. + * Caller is responsible for closing the connection (try-with-resources is recommended). + * + * @return new Connection + * @throws SQLException if the driver cannot open the database + */ + private static Connection getConnection() throws SQLException { + String url = "jdbc:sqlite:" + Helpers.DATABASE_PATH.toString(); + return DriverManager.getConnection(url); + } + + /** + * Get a student id by name, creating a new Student row when none exists. + * + * @param name student display name + * @return id of the existing or newly created student + * @throws SQLException on database errors + */ + public static int getOrCreateStudent(final String name) throws SQLException { + try (Connection c = getConnection()) { + try (PreparedStatement ps = c.prepareStatement("SELECT id FROM Student WHERE name = ?")) { + ps.setString(1, name); + try (ResultSet rs = ps.executeQuery()) { + if (rs.next()) { + return rs.getInt(1); + } + } + } + try (PreparedStatement ps = c.prepareStatement("INSERT INTO Student(name) VALUES (?)", Statement.RETURN_GENERATED_KEYS)) { + ps.setString(1, name); + ps.executeUpdate(); + try (ResultSet keys = ps.getGeneratedKeys()) { + if (keys.next()) { + return keys.getInt(1); + } + } + } + } + throw new SQLException("Failed to create or retrieve student"); + } + + /** + * Get or create a ProgressType row by name. + * + * @param name progress type display name + * @return database id of the progress type + * @throws SQLException on database errors + */ + public static int getOrCreateProgressType(final String name) throws SQLException { + try (Connection c = getConnection()) { + try (PreparedStatement ps = c.prepareStatement("SELECT id FROM ProgressType WHERE name = ?")) { + ps.setString(1, name); + try (ResultSet rs = ps.executeQuery()) { + if (rs.next()) { + return rs.getInt(1); + } + } + } + try (PreparedStatement ps = c.prepareStatement("INSERT INTO ProgressType(name) VALUES (?)", Statement.RETURN_GENERATED_KEYS)) { + ps.setString(1, name); + ps.executeUpdate(); + try (ResultSet keys = ps.getGeneratedKeys()) { + if (keys.next()) { + return keys.getInt(1); + } + } + } + } + throw new SQLException("Failed to create or retrieve ProgressType"); + } + + /** + * Ensure AssessmentPart rows exist for the given progress type. This uses + * SQL "INSERT OR IGNORE" so existing parts are preserved. + * + * @param progressTypeId id of the ProgressType + * @param codes array of part codes to ensure + * @throws SQLException on database errors + */ + public static void ensureAssessmentParts(final int progressTypeId, final String[] codes) throws SQLException { + try (Connection c = getConnection()) { + try (PreparedStatement ps = c.prepareStatement("INSERT OR IGNORE INTO AssessmentPart(progress_type_id, code, description) VALUES (?, ?, NULL)")) { + for (String code : codes) { + ps.setInt(1, progressTypeId); + ps.setString(2, code); + ps.addBatch(); + } + ps.executeBatch(); + } + } + } + + /** + * Remove any AssessmentPart rows for the given progress type whose code is + * not present in the provided canonical codes array. This helps clean up + * legacy/malformed entries that could cause part ordering mismatches. + * + * @param progressTypeId id of the ProgressType + * @param allowedCodes canonical set of codes to keep + * @throws SQLException on database errors + */ + public static void cleanupAssessmentParts(final int progressTypeId, final String[] allowedCodes) throws SQLException { + if (allowedCodes == null || allowedCodes.length == 0) { + return; + } + try (Connection c = getConnection()) { + StringBuilder sb = new StringBuilder(); + for (int i = 0; i < allowedCodes.length; i++) { + if (i > 0) { sb.append(','); } + sb.append('?'); + } + String sql = "DELETE FROM AssessmentPart WHERE progress_type_id = ? AND code NOT IN (" + sb.toString() + ")"; + try (PreparedStatement ps = c.prepareStatement(sql)) { + ps.setInt(1, progressTypeId); + for (int i = 0; i < allowedCodes.length; i++) { + ps.setString(i + 2, allowedCodes[i]); + } + ps.executeUpdate(); + } + } + } + + /** + * Create a ProgressSession for a student and progress type on the given date. + * + * @param studentId existing student id + * @param progressTypeId existing progress type id + * @param date session date + * @return generated ProgressSession id + * @throws SQLException on database errors + */ + public static int createProgressSession(final int studentId, final int progressTypeId, final LocalDate date) throws SQLException { + try (Connection c = getConnection()) { + try (PreparedStatement ps = c.prepareStatement("INSERT INTO ProgressSession(student_id, progress_type_id, date, notes) VALUES (?, ?, ?, NULL)", Statement.RETURN_GENERATED_KEYS)) { + ps.setInt(1, studentId); + ps.setInt(2, progressTypeId); + ps.setString(3, date.toString()); + ps.executeUpdate(); + try (ResultSet keys = ps.getGeneratedKeys()) { + if (keys.next()) { return keys.getInt(1); } + } + } + } + throw new SQLException("Failed to create ProgressSession"); + } + + /** + * Insert assessment results for a session. The {@code codes} and {@code scores} + * arrays must be parallel and correspond to existing AssessmentPart codes. + * Unknown part codes are ignored. + * + * @param sessionId progress session id + * @param progressTypeId progress type id + * @param codes array of part codes + * @param scores array of integer scores + * @throws SQLException on database errors + */ + public static void insertAssessmentResults(final int sessionId, final int progressTypeId, final String[] codes, final int[] scores) throws SQLException { + if (codes.length != scores.length) { throw new IllegalArgumentException("codes and scores length mismatch"); } + try (Connection c = getConnection()) { + // cache part ids + Map partIdMap = new HashMap<>(); + try (PreparedStatement ps = c.prepareStatement("SELECT id, code FROM AssessmentPart WHERE progress_type_id = ?")) { + ps.setInt(1, progressTypeId); + try (ResultSet rs = ps.executeQuery()) { + while (rs.next()) { + partIdMap.put(rs.getString("code"), rs.getInt("id")); + } + } + } + try (PreparedStatement ins = c.prepareStatement("INSERT INTO AssessmentResult(session_id, part_id, score) VALUES (?, ?, ?)") ) { + for (int i = 0; i < codes.length; i++) { + Integer partId = partIdMap.get(codes[i]); + if (partId == null) { + // skip unknown part + continue; + } + ins.setInt(1, sessionId); + ins.setInt(2, partId); + ins.setInt(3, scores[i]); + ins.addBatch(); + } + ins.executeBatch(); + } + } + } + + /** + * Fetch the latest assessment result rows for a named student and progress type. + * Each returned row is a list of integer scores for the parts in canonical + * part order. + * + * @param studentName student display name + * @param progressTypeName progress type display name + * @param limit maximum number of recent sessions to fetch + * @return list of rows, each row is a list of integer scores + * @throws SQLException on database errors + */ + public static List> fetchLatestAssessmentResults(final String studentName, final String progressTypeName, final int limit) throws SQLException { + List> result = new ArrayList<>(); + try (Connection c = getConnection()) { + Integer studentId = null; + try (PreparedStatement ps = c.prepareStatement("SELECT id FROM Student WHERE name = ?")) { + ps.setString(1, studentName); + try (ResultSet rs = ps.executeQuery()) { + if (rs.next()) { studentId = rs.getInt(1); } + } + } + if (studentId == null) { return result; } + + Integer progressTypeId = null; + try (PreparedStatement ps = c.prepareStatement("SELECT id FROM ProgressType WHERE name = ?")) { + ps.setString(1, progressTypeName); + try (ResultSet rs = ps.executeQuery()) { + if (rs.next()) { progressTypeId = rs.getInt(1); } + } + } + if (progressTypeId == null) { return result; } + + // get parts in canonical order (by id) + List partIds = new ArrayList<>(); + try (PreparedStatement ps = c.prepareStatement("SELECT id, code FROM AssessmentPart WHERE progress_type_id = ? ORDER BY id ASC")) { + ps.setInt(1, progressTypeId); + try (ResultSet rs = ps.executeQuery()) { + while (rs.next()) { partIds.add(rs.getInt("id")); } + } + } + + // get latest session ids for this student and progress type + List sessionIds = new ArrayList<>(); + try (PreparedStatement ps = c.prepareStatement("SELECT id FROM ProgressSession WHERE student_id = ? AND progress_type_id = ? ORDER BY id DESC LIMIT ?")) { + ps.setInt(1, studentId); + ps.setInt(2, progressTypeId); + ps.setInt(3, limit); + try (ResultSet rs = ps.executeQuery()) { + while (rs.next()) { sessionIds.add(rs.getInt(1)); } + } + } + + // For each session, fetch scores mapped to parts + for (Integer sid : sessionIds) { + Map scoreByPart = new HashMap<>(); + try (PreparedStatement ps = c.prepareStatement("SELECT part_id, score FROM AssessmentResult WHERE session_id = ?")) { + ps.setInt(1, sid); + try (ResultSet rs = ps.executeQuery()) { + while (rs.next()) { + scoreByPart.put(rs.getInt("part_id"), rs.getInt("score")); + } + } + } + List row = new ArrayList<>(); + for (Integer pid : partIds) { + Integer s = scoreByPart.get(pid); + row.add(s == null ? 0 : s); + } + result.add(row); + } + } + return result; + } + + /** + * Simple, immutable holder for time-series assessment results. + * + *

Contains a chronologically ordered list of session {@code dates} + * and a parallel list of integer score rows. Each entry in {@code rows} + * corresponds to the parts for a progress type in canonical order. + */ + public static class ResultsWithDates { + /** + * Ordered session dates (oldest first). Can be empty when no sessions exist. + */ + public final java.util.List dates; + + /** + * Parallel rows of integer scores. Each inner list corresponds to the + * assessment parts for a single session in canonical part order. May be + * empty when there are no sessions. + */ + public final java.util.List> rows; + + /** + * Create a ResultsWithDates instance. + * + * @param dates ordered session dates (oldest-first) + * @param rows parallel list of score rows matching {@code dates} + */ + public ResultsWithDates(java.util.List dates, java.util.List> rows) { + this.dates = dates; + this.rows = rows; + } + } + + /** + * Fetch the latest assessment rows along with their session dates. + * Rows and dates are ordered oldest-first to facilitate time series plotting. + * + * @param studentName student display name to filter results for + * @param progressTypeName progress type display name (e.g., "Braille") + * @param limit maximum number of recent sessions to return + * @return ResultsWithDates holding an ordered list of session dates and parallel rows of scores + * @throws SQLException on database errors + */ + public static ResultsWithDates fetchLatestAssessmentResultsWithDates(final String studentName, final String progressTypeName, final int limit) throws SQLException { + java.util.List> result = new ArrayList<>(); + java.util.List dates = new ArrayList<>(); + try (Connection c = getConnection()) { + Integer studentId = null; + try (PreparedStatement ps = c.prepareStatement("SELECT id FROM Student WHERE name = ?")) { + ps.setString(1, studentName); + try (ResultSet rs = ps.executeQuery()) { + if (rs.next()) { + studentId = rs.getInt(1); + } + } + } + if (studentId == null) { return new ResultsWithDates(dates, result); } + + Integer progressTypeId = null; + try (PreparedStatement ps = c.prepareStatement("SELECT id FROM ProgressType WHERE name = ?")) { + ps.setString(1, progressTypeName); + try (ResultSet rs = ps.executeQuery()) { + if (rs.next()) { + progressTypeId = rs.getInt(1); + } + } + } + if (progressTypeId == null) { + return new ResultsWithDates(dates, result); + } + + // get parts in canonical order (by id) + java.util.List partIds = new ArrayList<>(); + try (PreparedStatement ps = c.prepareStatement("SELECT id, code FROM AssessmentPart WHERE progress_type_id = ? ORDER BY id ASC")) { + ps.setInt(1, progressTypeId); + try (ResultSet rs = ps.executeQuery()) { + while (rs.next()) { + partIds.add(rs.getInt("id")); + } + } + } + + // get latest session ids and dates for this student and progress type + java.util.List sessionIds = new ArrayList<>(); + java.util.List sessionDates = new ArrayList<>(); + try (PreparedStatement ps = c.prepareStatement("SELECT id, date FROM ProgressSession WHERE student_id = ? AND progress_type_id = ? ORDER BY id DESC LIMIT ?")) { + ps.setInt(1, studentId); ps.setInt(2, progressTypeId); ps.setInt(3, limit); + try (ResultSet rs = ps.executeQuery()) { + while (rs.next()) { + sessionIds.add(rs.getInt("id")); + sessionDates.add(java.time.LocalDate.parse(rs.getString("date"))); + } + } + } + + // We want chronological order (oldest first) + java.util.Collections.reverse(sessionIds); + java.util.Collections.reverse(sessionDates); + + // For each session, fetch scores mapped to parts and append row + for (Integer sid : sessionIds) { + Map scoreByPart = new HashMap<>(); + try (PreparedStatement ps = c.prepareStatement("SELECT part_id, score FROM AssessmentResult WHERE session_id = ?")) { + ps.setInt(1, sid); + try (ResultSet rs = ps.executeQuery()) { + while (rs.next()) { + scoreByPart.put(rs.getInt("part_id"), rs.getInt("score")); + } + } + } + java.util.List row = new ArrayList<>(); + for (Integer pid : partIds) { + Integer s = scoreByPart.get(pid); + row.add(s == null ? 0 : s); + } + result.add(row); + } + dates.addAll(sessionDates); + } + return new ResultsWithDates(dates, result); + } + + /** + * Insert a keyboarding-specific result linked to a ProgressSession. + * + * @param sessionId existing session id + * @param program program or curriculum name + * @param topic topic or lesson name + * @param speed words-per-minute + * @param accuracy accuracy percent + * @throws SQLException on database errors + */ + public static void insertKeyboardingResult(final int sessionId, final String program, final String topic, final int speed, final int accuracy) throws SQLException { + try (Connection c = getConnection()) { + try (PreparedStatement ps = c.prepareStatement("INSERT INTO KeyboardingResult(session_id, program, topic, speed, accuracy) VALUES (?, ?, ?, ?, ?)")) { + ps.setInt(1, sessionId); + ps.setString(2, program); + ps.setString(3, topic); + ps.setInt(4, speed); + ps.setInt(5, accuracy); + ps.executeUpdate(); + } + } + } + + /** + * Save free-form notes for a given ProgressSession. + * + * @param sessionId progress session id + * @param notes free-form notes text + * @throws SQLException on database errors + */ + public static void saveSessionNotes(final int sessionId, final String notes) throws SQLException { + try (Connection c = getConnection()) { + try (PreparedStatement ps = c.prepareStatement("UPDATE ProgressSession SET notes = ? WHERE id = ?")) { + ps.setString(1, notes); + ps.setInt(2, sessionId); + ps.executeUpdate(); + } + } + } + + /** + * Save structured contact log details for a given ProgressSession. This + * will insert or replace a single ContactLog row tied to the session. + * + * @param sessionId existing session id + * @param studentName student display name + * @param date session date as text + * @param guardianName guardian or parent name + * @param contactMethod method of contact (phone/email/etc) + * @param phoneNumber phone number string + * @param emailAddress email address string + * @param contactResponse short description of response + * @param contactGeneral general contact summary + * @param contactSpecific specific items discussed + * @param contactNotes free-form notes + * @throws SQLException on database errors + */ + public static void saveContactLog(final int sessionId, final String studentName, final String date, final String guardianName, final String contactMethod, final String phoneNumber, final String emailAddress, final String contactResponse, final String contactGeneral, final String contactSpecific, final String contactNotes) throws SQLException { + try (Connection c = getConnection()) { + try (PreparedStatement ps = c.prepareStatement("INSERT OR REPLACE INTO ContactLog(session_id, student_name, date, guardian_name, contact_method, phone_number, email_address, contact_response, contact_general, contact_specific, contact_notes) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)") ) { + ps.setInt(1, sessionId); + ps.setString(2, studentName); + ps.setString(3, date); + ps.setString(4, guardianName); + ps.setString(5, contactMethod); + ps.setString(6, phoneNumber); + ps.setString(7, emailAddress); + ps.setString(8, contactResponse); + ps.setString(9, contactGeneral); + ps.setString(10, contactSpecific); + ps.setString(11, contactNotes); + ps.executeUpdate(); + } + } + } + + /** + * Fetch the most recent ContactLog entry for the given student name. + * Returns a map of column names to string values, or null if none found. + * + * @param studentName student display name to search for + * @return map of contact log columns to values or null when not found + * @throws SQLException on database errors + */ + public static com.studentgui.apphelpers.dto.ContactPayload fetchLatestContactLog(final String studentName) throws SQLException { + try (Connection c = getConnection()) { + Integer studentId = null; + try (PreparedStatement ps = c.prepareStatement("SELECT id FROM Student WHERE name = ?")) { + ps.setString(1, studentName); + try (ResultSet rs = ps.executeQuery()) { + if (rs.next()) { + studentId = rs.getInt(1); + } + } + } + if (studentId == null) { + return null; + } + + // Find the latest session id for ProgressType 'ContactLog' + Integer ptId = null; + try (PreparedStatement ps = c.prepareStatement("SELECT id FROM ProgressType WHERE name = ?")) { + ps.setString(1, "ContactLog"); + try (ResultSet rs = ps.executeQuery()) { + if (rs.next()) { + ptId = rs.getInt(1); + } + } + } + if (ptId == null) { + return null; + } + + Integer sessionId = null; + try (PreparedStatement ps = c.prepareStatement("SELECT id FROM ProgressSession WHERE student_id = ? AND progress_type_id = ? ORDER BY id DESC LIMIT 1")) { + ps.setInt(1, studentId); + ps.setInt(2, ptId); + try (ResultSet rs = ps.executeQuery()) { + if (rs.next()) { + sessionId = rs.getInt(1); + } + } + } + if (sessionId == null) { + return null; + } + + try (PreparedStatement ps = c.prepareStatement("SELECT student_name, date, guardian_name, contact_method, phone_number, email_address, contact_response, contact_general, contact_specific, contact_notes FROM ContactLog WHERE session_id = ? ORDER BY id DESC LIMIT 1")) { + ps.setInt(1, sessionId); + try (ResultSet rs = ps.executeQuery()) { + if (rs.next()) { + com.studentgui.apphelpers.dto.ContactPayload p = new com.studentgui.apphelpers.dto.ContactPayload( + sessionId, + rs.getString("guardian_name"), + rs.getString("contact_method"), + rs.getString("phone_number"), + rs.getString("email_address"), + rs.getString("contact_response"), + rs.getString("contact_general"), + rs.getString("contact_specific"), + rs.getString("contact_notes") + ); + return p; + } + } + } + return null; + } + } + +} diff --git a/src/main/java/com/studentgui/apphelpers/Helpers.java b/src/main/java/com/studentgui/apphelpers/Helpers.java index 30b86d9..a0a3f33 100644 --- a/src/main/java/com/studentgui/apphelpers/Helpers.java +++ b/src/main/java/com/studentgui/apphelpers/Helpers.java @@ -1,299 +1,299 @@ -package com.studentgui.apphelpers; - -import java.io.IOException; -import java.nio.file.Files; -import java.nio.file.Path; -import java.nio.file.Paths; -import java.util.ArrayList; -import java.util.List; - -/** - * Miscellaneous filesystem and small utility helpers used by the UI pages. - * - * Responsibilities include selecting and creating the application home - * directory, creating per-student folder hierarchies, and providing a - * small roster fallback when no students.json exists. - */ -public class Helpers { - /** - * Private constructor to prevent instantiation of this utility class. - */ - private Helpers() { - throw new AssertionError("Helpers is a utility class"); - } - private static final org.slf4j.Logger LOG = org.slf4j.LoggerFactory.getLogger(Helpers.class); - /** The project working directory (where the process was started). */ - public static final Path PROJECT_ROOT = Paths.get(System.getProperty("user.dir")); - /** Application home used for storing app-specific files (defaults to ./app_home). */ - public static final Path APP_HOME = selectAppHome(); - /** Root directory for persisted application data (alias of APP_HOME). */ - public static final Path DATA_ROOT = APP_HOME; - /** Directory that holds the database file. */ - public static final Path DATABASE_ROOT = DATA_ROOT.resolve("StudentDatabase"); - /** Canonical database file path used by SQLite operations. */ - public static final Path DATABASE_PATH = DATABASE_ROOT.resolve("students20252026.db"); - - /** - * Select a suitable application home directory. Attempts to use a - * ./app_home subdirectory of the working directory and falls back to the - * system temporary directory if creation fails. - */ - private static Path selectAppHome() { - try { - Path candidate = PROJECT_ROOT.resolve("app_home"); - Files.createDirectories(candidate); - // test write - Path test = candidate.resolve(".write_test"); - Files.writeString(test, ""); - Files.deleteIfExists(test); - return candidate; - } catch (IOException e) { - LOG.debug("Unable to create app_home; falling back to temp dir", e); - try { - Path tmp = Paths.get(System.getProperty("java.io.tmpdir"), "StudentDataGUI"); - Files.createDirectories(tmp); - return tmp; - } catch (IOException ex) { - LOG.debug("Unable to create fallback temp dir; using CWD", ex); - return Paths.get("."); - } - } - } - - /** - * Attempt to set the JVM working directory to APP_HOME. Fails silently if - * the property cannot be set in the running environment. - */ - public static void setStartDir() { - /** - * Set the JVM working directory to the application home when possible. - * Fail silently if the property cannot be set. - */ - try { - System.setProperty("user.dir", APP_HOME.toString()); - } catch (SecurityException se) { - LOG.debug("Unable to set user.dir to APP_HOME {}", APP_HOME, se); - } - } - - /** - * Ensure the working data directory exists under APP_HOME. This is - * idempotent and safe to call on startup. - */ - public static void workingDir() { - /** - * Ensure the working data directory exists under the application home. - */ - try { - Path studentDataDir = APP_HOME.resolve("StudentDataFiles"); - Files.createDirectories(studentDataDir); - } catch (IOException ioe) { - LOG.debug("Unable to create StudentDataFiles directory under {}", APP_HOME, ioe); - } - } - - /** - * Create a basic folder hierarchy under DATA_ROOT for each student. - * This will create StudentDataFiles, backups and errorLogs and a - * per-student folder with subfolders for data sheets and materials. - */ - public static void createFolderHierarchy() { - /** - * Create a basic folder hierarchy under DATA_ROOT for each student. - * This is idempotent and will create per-student subfolders and an - * omnibus csv file when missing. - */ - // Create basic folders for each student in a simple roster - List students = getStudents(); - Path studentDatafilesRoot = DATA_ROOT.resolve("StudentDataFiles"); - Path studentErrorlogsRoot = DATA_ROOT.resolve("errorLogs"); - Path studentBackupsRoot = DATA_ROOT.resolve("backups"); - try { - Files.createDirectories(studentDatafilesRoot); - Files.createDirectories(studentErrorlogsRoot); - Files.createDirectories(studentBackupsRoot); - } catch (IOException ioe) { - LOG.debug("Unable to create one or more data folders under {}", DATA_ROOT, ioe); - } - - for (String name : students) { - String safe = sanitize(name); - Path studentFolder = studentDatafilesRoot.resolve(safe); - try { - Files.createDirectories(studentFolder.resolve("StudentDataSheets")); - Files.createDirectories(studentFolder.resolve("StudentInstructionMaterials")); - Files.createDirectories(studentFolder.resolve("StudentVisionAssessments")); - Path omnibus = studentFolder.resolve("omnibusDatabase.csv"); - if (!Files.exists(omnibus)) { - Files.createFile(omnibus); - } - } catch (IOException ioe) { - LOG.debug("Unable to create per-student folder or omnibus file for {}", name, ioe); - } - } - } - - /** - * Make a filesystem-safe folder name by stripping or replacing forbidden - * characters. - */ - private static String sanitize(final String s) { - if (s == null) { - return ""; - } - String t = s.trim(); - // remove control characters (newline, carriage return, etc.) - t = t.replaceAll("[\\p{Cntrl}]", ""); - // replace common filesystem-forbidden characters with underscore - char[] forbidden = new char[]{'<','>',';',':','"','/','\\','|','?','*'}; - for (char c : forbidden) { - t = t.replace(c, '_'); - } - // collapse runs of whitespace into single space - t = t.replaceAll("\\s+", " ").trim(); - // prevent names that are just dots - if (t.matches("^[.]+$")) { - t = "_"; - } - return t; - } - - /** - * Public safe name helper for filesystem paths. Mirrors the internal - * sanitize implementation but is callable from other packages. - * - * @param s input display name - * @return sanitized filesystem-safe name (never null) - */ - public static String safeName(final String s) { - if (s == null) { - return ""; - } - return sanitize(s); - } - - /** - * Find the latest PNG plot file for a named student with the given prefix. - * Returns null when no matching files exist. - * - * @param studentName display name of student - * @param prefix file prefix such as "iOS" or "ScreenReader" - * @return path to the most recently modified matching PNG, or null - */ - public static java.nio.file.Path latestPlotPath(final String studentName, final String prefix) { - if (studentName == null || studentName.trim().isEmpty()) { - return null; - } - java.nio.file.Path dir = studentPlotsDir(studentName); - if (!java.nio.file.Files.exists(dir)) { - return null; - } - java.nio.file.Path latest = null; - try (java.nio.file.DirectoryStream ds = java.nio.file.Files.newDirectoryStream(dir, prefix + "-*.png")) { - for (java.nio.file.Path p : ds) { - try { - if (latest == null) { - latest = p; - } else { - java.nio.file.attribute.FileTime t1 = java.nio.file.Files.getLastModifiedTime(p); - java.nio.file.attribute.FileTime t2 = java.nio.file.Files.getLastModifiedTime(latest); - if (t1.compareTo(t2) > 0) { - latest = p; - } - } - } catch (IOException ioe) { - LOG.debug("Error reading file metadata for {}", p, ioe); - } - } - } catch (IOException ioe) { - LOG.debug("Error listing plot directory {}", dir, ioe); - } - return latest; - } - - /** - * Return the per-student plots directory path (APP_HOME/StudentDataFiles/{safeName}/plots). - * - * @param studentName display name of the student - * @return path to the student's plots directory (never null) - */ - public static java.nio.file.Path studentPlotsDir(final String studentName) { - return APP_HOME.resolve("StudentDataFiles").resolve(safeName(studentName)).resolve("plots"); - } - - /** - * Return the per-student reports directory path (APP_HOME/StudentDataFiles/{safeName}/reports). - * - * @param studentName display name of the student - * @return path to the student's reports directory (never null) - */ - public static java.nio.file.Path studentReportsDir(final String studentName) { - return APP_HOME.resolve("StudentDataFiles").resolve(safeName(studentName)).resolve("reports"); - } - - /** - * Return the per-student collected data directory path (APP_HOME/StudentDataFiles/{safeName}/collected_data). - * - * @param studentName display name of the student - * @return path to the student's collected data directory (never null) - */ - public static java.nio.file.Path studentCollectedDataDir(final String studentName) { - return APP_HOME.resolve("StudentDataFiles").resolve(safeName(studentName)).resolve("collected_data"); - } - - /** - * Attempt to return a simple list of students from PROJECT_ROOT/json_Files/students.json. - * Falls back to a single 'Test Student' entry when the file is missing or cannot be read. - * - * @return list of student display names (never null) - */ - public static List getStudents() { - // Attempt to read a simple students.json in PROJECT_ROOT/json_Files/students.json - List list = new ArrayList<>(); - Path p = PROJECT_ROOT.resolve("json_Files").resolve("students.json"); - if (Files.exists(p)) { - try { - String text = Files.readString(p); - // try to isolate the array portion if present - int start = text.indexOf('['); - int end = text.lastIndexOf(']'); - String body = (start >= 0 && end > start) ? text.substring(start, end + 1) : text; - java.util.regex.Pattern pat = java.util.regex.Pattern.compile("\"([^\"]+)\""); - java.util.regex.Matcher m = pat.matcher(body); - while (m.find()) { - String candidate = m.group(1).trim(); - if (!candidate.isEmpty()) { - list.add(candidate); - } - } - } catch (IOException ioe) { - LOG.debug("Unable to read students.json {}", p, ioe); - } - } - if (list.isEmpty()) { - // fallback roster - list.add("Test Student"); - } - return list; - } - - /** - * Return the default student to use when none is provided by the caller. - * This is the first entry from getStudents() or a sensible fallback when - * the roster is empty. - * - * @return display name of the default student (never null) - */ - public static String defaultStudent() { - /** - * Note: UI pages use this helper to provide a non-null default student - * when constructed with a null/empty student name so that charts and - * page logic can operate without requiring an immediate user selection. - */ - List s = getStudents(); - if (s == null || s.isEmpty()) { - return "Demo Student"; - } - return s.get(0); - } -} +package com.studentgui.apphelpers; + +import java.io.IOException; +import java.nio.file.Files; +import java.nio.file.Path; +import java.nio.file.Paths; +import java.util.ArrayList; +import java.util.List; + +/** + * Miscellaneous filesystem and small utility helpers used by the UI pages. + * + * Responsibilities include selecting and creating the application home + * directory, creating per-student folder hierarchies, and providing a + * small roster fallback when no students.json exists. + */ +public class Helpers { + /** + * Private constructor to prevent instantiation of this utility class. + */ + private Helpers() { + throw new AssertionError("Helpers is a utility class"); + } + private static final org.slf4j.Logger LOG = org.slf4j.LoggerFactory.getLogger(Helpers.class); + /** The project working directory (where the process was started). */ + public static final Path PROJECT_ROOT = Paths.get(System.getProperty("user.dir")); + /** Application home used for storing app-specific files (defaults to ./app_home). */ + public static final Path APP_HOME = selectAppHome(); + /** Root directory for persisted application data (alias of APP_HOME). */ + public static final Path DATA_ROOT = APP_HOME; + /** Directory that holds the database file. */ + public static final Path DATABASE_ROOT = DATA_ROOT.resolve("StudentDatabase"); + /** Canonical database file path used by SQLite operations. */ + public static final Path DATABASE_PATH = DATABASE_ROOT.resolve("students20252026.db"); + + /** + * Select a suitable application home directory. Attempts to use a + * ./app_home subdirectory of the working directory and falls back to the + * system temporary directory if creation fails. + */ + private static Path selectAppHome() { + try { + Path candidate = PROJECT_ROOT.resolve("app_home"); + Files.createDirectories(candidate); + // test write + Path test = candidate.resolve(".write_test"); + Files.writeString(test, ""); + Files.deleteIfExists(test); + return candidate; + } catch (IOException e) { + LOG.debug("Unable to create app_home; falling back to temp dir", e); + try { + Path tmp = Paths.get(System.getProperty("java.io.tmpdir"), "StudentDataGUI"); + Files.createDirectories(tmp); + return tmp; + } catch (IOException ex) { + LOG.debug("Unable to create fallback temp dir; using CWD", ex); + return Paths.get("."); + } + } + } + + /** + * Attempt to set the JVM working directory to APP_HOME. Fails silently if + * the property cannot be set in the running environment. + */ + public static void setStartDir() { + /** + * Set the JVM working directory to the application home when possible. + * Fail silently if the property cannot be set. + */ + try { + System.setProperty("user.dir", APP_HOME.toString()); + } catch (SecurityException se) { + LOG.debug("Unable to set user.dir to APP_HOME {}", APP_HOME, se); + } + } + + /** + * Ensure the working data directory exists under APP_HOME. This is + * idempotent and safe to call on startup. + */ + public static void workingDir() { + /** + * Ensure the working data directory exists under the application home. + */ + try { + Path studentDataDir = APP_HOME.resolve("StudentDataFiles"); + Files.createDirectories(studentDataDir); + } catch (IOException ioe) { + LOG.debug("Unable to create StudentDataFiles directory under {}", APP_HOME, ioe); + } + } + + /** + * Create a basic folder hierarchy under DATA_ROOT for each student. + * This will create StudentDataFiles, backups and errorLogs and a + * per-student folder with subfolders for data sheets and materials. + */ + public static void createFolderHierarchy() { + /** + * Create a basic folder hierarchy under DATA_ROOT for each student. + * This is idempotent and will create per-student subfolders and an + * omnibus csv file when missing. + */ + // Create basic folders for each student in a simple roster + List students = getStudents(); + Path studentDatafilesRoot = DATA_ROOT.resolve("StudentDataFiles"); + Path studentErrorlogsRoot = DATA_ROOT.resolve("errorLogs"); + Path studentBackupsRoot = DATA_ROOT.resolve("backups"); + try { + Files.createDirectories(studentDatafilesRoot); + Files.createDirectories(studentErrorlogsRoot); + Files.createDirectories(studentBackupsRoot); + } catch (IOException ioe) { + LOG.debug("Unable to create one or more data folders under {}", DATA_ROOT, ioe); + } + + for (String name : students) { + String safe = sanitize(name); + Path studentFolder = studentDatafilesRoot.resolve(safe); + try { + Files.createDirectories(studentFolder.resolve("StudentDataSheets")); + Files.createDirectories(studentFolder.resolve("StudentInstructionMaterials")); + Files.createDirectories(studentFolder.resolve("StudentVisionAssessments")); + Path omnibus = studentFolder.resolve("omnibusDatabase.csv"); + if (!Files.exists(omnibus)) { + Files.createFile(omnibus); + } + } catch (IOException ioe) { + LOG.debug("Unable to create per-student folder or omnibus file for {}", name, ioe); + } + } + } + + /** + * Make a filesystem-safe folder name by stripping or replacing forbidden + * characters. + */ + private static String sanitize(final String s) { + if (s == null) { + return ""; + } + String t = s.trim(); + // remove control characters (newline, carriage return, etc.) + t = t.replaceAll("[\\p{Cntrl}]", ""); + // replace common filesystem-forbidden characters with underscore + char[] forbidden = new char[]{'<','>',';',':','"','/','\\','|','?','*'}; + for (char c : forbidden) { + t = t.replace(c, '_'); + } + // collapse runs of whitespace into single space + t = t.replaceAll("\\s+", " ").trim(); + // prevent names that are just dots + if (t.matches("^[.]+$")) { + t = "_"; + } + return t; + } + + /** + * Public safe name helper for filesystem paths. Mirrors the internal + * sanitize implementation but is callable from other packages. + * + * @param s input display name + * @return sanitized filesystem-safe name (never null) + */ + public static String safeName(final String s) { + if (s == null) { + return ""; + } + return sanitize(s); + } + + /** + * Find the latest PNG plot file for a named student with the given prefix. + * Returns null when no matching files exist. + * + * @param studentName display name of student + * @param prefix file prefix such as "iOS" or "ScreenReader" + * @return path to the most recently modified matching PNG, or null + */ + public static java.nio.file.Path latestPlotPath(final String studentName, final String prefix) { + if (studentName == null || studentName.trim().isEmpty()) { + return null; + } + java.nio.file.Path dir = studentPlotsDir(studentName); + if (!java.nio.file.Files.exists(dir)) { + return null; + } + java.nio.file.Path latest = null; + try (java.nio.file.DirectoryStream ds = java.nio.file.Files.newDirectoryStream(dir, prefix + "-*.png")) { + for (java.nio.file.Path p : ds) { + try { + if (latest == null) { + latest = p; + } else { + java.nio.file.attribute.FileTime t1 = java.nio.file.Files.getLastModifiedTime(p); + java.nio.file.attribute.FileTime t2 = java.nio.file.Files.getLastModifiedTime(latest); + if (t1.compareTo(t2) > 0) { + latest = p; + } + } + } catch (IOException ioe) { + LOG.debug("Error reading file metadata for {}", p, ioe); + } + } + } catch (IOException ioe) { + LOG.debug("Error listing plot directory {}", dir, ioe); + } + return latest; + } + + /** + * Return the per-student plots directory path (APP_HOME/StudentDataFiles/{safeName}/plots). + * + * @param studentName display name of the student + * @return path to the student's plots directory (never null) + */ + public static java.nio.file.Path studentPlotsDir(final String studentName) { + return APP_HOME.resolve("StudentDataFiles").resolve(safeName(studentName)).resolve("plots"); + } + + /** + * Return the per-student reports directory path (APP_HOME/StudentDataFiles/{safeName}/reports). + * + * @param studentName display name of the student + * @return path to the student's reports directory (never null) + */ + public static java.nio.file.Path studentReportsDir(final String studentName) { + return APP_HOME.resolve("StudentDataFiles").resolve(safeName(studentName)).resolve("reports"); + } + + /** + * Return the per-student collected data directory path (APP_HOME/StudentDataFiles/{safeName}/collected_data). + * + * @param studentName display name of the student + * @return path to the student's collected data directory (never null) + */ + public static java.nio.file.Path studentCollectedDataDir(final String studentName) { + return APP_HOME.resolve("StudentDataFiles").resolve(safeName(studentName)).resolve("collected_data"); + } + + /** + * Attempt to return a simple list of students from PROJECT_ROOT/json_Files/students.json. + * Falls back to a single 'Test Student' entry when the file is missing or cannot be read. + * + * @return list of student display names (never null) + */ + public static List getStudents() { + // Attempt to read a simple students.json in PROJECT_ROOT/json_Files/students.json + List list = new ArrayList<>(); + Path p = PROJECT_ROOT.resolve("json_Files").resolve("students.json"); + if (Files.exists(p)) { + try { + String text = Files.readString(p); + // try to isolate the array portion if present + int start = text.indexOf('['); + int end = text.lastIndexOf(']'); + String body = (start >= 0 && end > start) ? text.substring(start, end + 1) : text; + java.util.regex.Pattern pat = java.util.regex.Pattern.compile("\"([^\"]+)\""); + java.util.regex.Matcher m = pat.matcher(body); + while (m.find()) { + String candidate = m.group(1).trim(); + if (!candidate.isEmpty()) { + list.add(candidate); + } + } + } catch (IOException ioe) { + LOG.debug("Unable to read students.json {}", p, ioe); + } + } + if (list.isEmpty()) { + // fallback roster + list.add("Test Student"); + } + return list; + } + + /** + * Return the default student to use when none is provided by the caller. + * This is the first entry from getStudents() or a sensible fallback when + * the roster is empty. + * + * @return display name of the default student (never null) + */ + public static String defaultStudent() { + /** + * Note: UI pages use this helper to provide a non-null default student + * when constructed with a null/empty student name so that charts and + * page logic can operate without requiring an immediate user selection. + */ + List s = getStudents(); + if (s == null || s.isEmpty()) { + return "Demo Student"; + } + return s.get(0); + } +} diff --git a/src/main/java/com/studentgui/apphelpers/PythonPlotter.java b/src/main/java/com/studentgui/apphelpers/PythonPlotter.java index 929d1d6..4f3c33d 100644 --- a/src/main/java/com/studentgui/apphelpers/PythonPlotter.java +++ b/src/main/java/com/studentgui/apphelpers/PythonPlotter.java @@ -1,85 +1,85 @@ -package com.studentgui.apphelpers; - -import java.io.BufferedReader; -import java.io.InputStreamReader; -import java.nio.file.Path; -import java.util.function.Consumer; - -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; - -/** - * Helper to invoke the repository's Python plot runner asynchronously. - *

- * This wrapper launches the repository's Python runner script in a - * background thread and collects its combined output. It is used by some - * legacy pages; newer pages prefer the Java-based charting helpers. - *

- */ -public class PythonPlotter { - private static final Logger LOG = LoggerFactory.getLogger(PythonPlotter.class); - - /** - * Run the python runner for the given module and student name in a background thread. - * The {@code onComplete} consumer receives combined stdout/stderr text when the - * process finishes. - * - * @param moduleName module identifier passed to the python runner (non-null) - * @param studentName student display name used by the plotter (non-null) - * @param onComplete optional consumer receiving process output when complete; may be null - */ - public static void runPlotAsync(final String moduleName, final String studentName, final Consumer onComplete) { - if (studentName == null || studentName.trim().isEmpty()) { - String msg = "No student selected for plot generation"; - LOG.warn(msg); - if (onComplete != null) { - onComplete.accept(msg); - } - return; - } - - Path script = Helpers.PROJECT_ROOT.resolve("appPages").resolve("run_plot.py"); - - Thread t = new Thread(() -> { - StringBuilder out = new StringBuilder(); - try { - ProcessBuilder pb = new ProcessBuilder("python", script.toString(), moduleName, studentName); - pb.directory(Helpers.PROJECT_ROOT.toFile()); - pb.redirectErrorStream(true); - Process p = pb.start(); - try (BufferedReader r = new BufferedReader(new InputStreamReader(p.getInputStream()))) { - String line; - while ((line = r.readLine()) != null) { - out.append(line).append(System.lineSeparator()); - } - } - int rc = p.waitFor(); - out.append("Exit code: ").append(rc).append(System.lineSeparator()); - } catch (java.io.IOException | InterruptedException e) { - LOG.error("Error running python plot runner", e); - out.append("Error: ").append(e.toString()).append(System.lineSeparator()); - if (e instanceof InterruptedException) { - Thread.currentThread().interrupt(); - } - } - if (onComplete != null) { - try { - onComplete.accept(out.toString()); - } catch (Exception ex) { - // Log and continue: we don't want a faulty consumer to terminate - // the worker thread unexpectedly. Avoid catching Error/Throwable. - LOG.warn("onComplete consumer threw an exception; continuing. Output length={}", out.length(), ex); - } - } - }, "PythonPlotter-" + moduleName); - t.setDaemon(true); - t.start(); - } - - /** - * Private constructor to prevent instantiation of this helper class. - */ - private PythonPlotter() { - // utility only - } -} +package com.studentgui.apphelpers; + +import java.io.BufferedReader; +import java.io.InputStreamReader; +import java.nio.file.Path; +import java.util.function.Consumer; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + * Helper to invoke the repository's Python plot runner asynchronously. + *

+ * This wrapper launches the repository's Python runner script in a + * background thread and collects its combined output. It is used by some + * legacy pages; newer pages prefer the Java-based charting helpers. + *

+ */ +public class PythonPlotter { + private static final Logger LOG = LoggerFactory.getLogger(PythonPlotter.class); + + /** + * Run the python runner for the given module and student name in a background thread. + * The {@code onComplete} consumer receives combined stdout/stderr text when the + * process finishes. + * + * @param moduleName module identifier passed to the python runner (non-null) + * @param studentName student display name used by the plotter (non-null) + * @param onComplete optional consumer receiving process output when complete; may be null + */ + public static void runPlotAsync(final String moduleName, final String studentName, final Consumer onComplete) { + if (studentName == null || studentName.trim().isEmpty()) { + String msg = "No student selected for plot generation"; + LOG.warn(msg); + if (onComplete != null) { + onComplete.accept(msg); + } + return; + } + + Path script = Helpers.PROJECT_ROOT.resolve("appPages").resolve("run_plot.py"); + + Thread t = new Thread(() -> { + StringBuilder out = new StringBuilder(); + try { + ProcessBuilder pb = new ProcessBuilder("python", script.toString(), moduleName, studentName); + pb.directory(Helpers.PROJECT_ROOT.toFile()); + pb.redirectErrorStream(true); + Process p = pb.start(); + try (BufferedReader r = new BufferedReader(new InputStreamReader(p.getInputStream()))) { + String line; + while ((line = r.readLine()) != null) { + out.append(line).append(System.lineSeparator()); + } + } + int rc = p.waitFor(); + out.append("Exit code: ").append(rc).append(System.lineSeparator()); + } catch (java.io.IOException | InterruptedException e) { + LOG.error("Error running python plot runner", e); + out.append("Error: ").append(e.toString()).append(System.lineSeparator()); + if (e instanceof InterruptedException) { + Thread.currentThread().interrupt(); + } + } + if (onComplete != null) { + try { + onComplete.accept(out.toString()); + } catch (Exception ex) { + // Log and continue: we don't want a faulty consumer to terminate + // the worker thread unexpectedly. Avoid catching Error/Throwable. + LOG.warn("onComplete consumer threw an exception; continuing. Output length={}", out.length(), ex); + } + } + }, "PythonPlotter-" + moduleName); + t.setDaemon(true); + t.start(); + } + + /** + * Private constructor to prevent instantiation of this helper class. + */ + private PythonPlotter() { + // utility only + } +} diff --git a/src/main/java/com/studentgui/apphelpers/SessionJsonWriter.java b/src/main/java/com/studentgui/apphelpers/SessionJsonWriter.java index 01b8cdb..d927f2d 100644 --- a/src/main/java/com/studentgui/apphelpers/SessionJsonWriter.java +++ b/src/main/java/com/studentgui/apphelpers/SessionJsonWriter.java @@ -1,125 +1,125 @@ -package com.studentgui.apphelpers; - -import java.io.IOException; -import java.nio.file.Files; -import java.nio.file.Path; -import java.time.Instant; -import java.time.ZoneId; -import java.time.format.DateTimeFormatter; -import java.util.HashMap; -import java.util.Map; - -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; - -import com.fasterxml.jackson.databind.ObjectMapper; - -/** - * Helper to write per-session JSON exports for app pages. - */ -public final class SessionJsonWriter { - private static final Logger LOG = LoggerFactory.getLogger(SessionJsonWriter.class); - private static final ObjectMapper MAPPER = new ObjectMapper(); - - private SessionJsonWriter() {} - - /** - * Write a per-session JSON file into the student's StudentDataFiles folder. - * The filename will include a unix timestamp to ensure uniqueness per session. - * - * @param student display name of the student - * @param pageName short page identifier (e.g. "Abacus") - * @param payload arbitrary payload object to serialize (Map or POJO) - * @return the path to the written file, or null on failure - */ - public static Path writeSessionJson(final String student, final String pageName, final Object payload) { - return writeSessionJson(student, pageName, payload, null); - } - - /** - * Write a per-session JSON file and optionally include an explicit sessionId. - * If the explicit sessionId is null, this method will look for a "sessionId" - * entry inside the payload Map and use that if present. The envelope written - * to disk will include the sessionId when available. - * - * Filename format: {@code PageName--[-session-].json} - * - * @param student display name of the student - * @param pageName short page identifier (e.g. "Abacus") - * @param payload arbitrary payload object to serialize (Map or POJO) - * @param explicitSessionId optional session id to use in the envelope and filename - * @return the path to the written file, or null on failure - */ - public static Path writeSessionJson(final String student, final String pageName, final Object payload, final String explicitSessionId) { - if (student == null || student.trim().isEmpty() || pageName == null) { - return null; - } - try { - Path outDir = Helpers.studentCollectedDataDir(student); - Files.createDirectories(outDir); - long ts = Instant.now().toEpochMilli(); - // format for readability too - String readable = DateTimeFormatter.ofPattern("yyyyMMdd-HHmmssSSS").withZone(ZoneId.systemDefault()).format(Instant.ofEpochMilli(ts)); - - // Determine sessionId preference: explicit param first, then payload if it implements SessionPayload - String sid = explicitSessionId; - if (sid == null && payload instanceof com.studentgui.apphelpers.dto.SessionPayload) { - int s = ((com.studentgui.apphelpers.dto.SessionPayload) payload).getSessionId(); - if (s != 0) { - sid = Integer.toString(s); - } - } - - String filename = String.format("%s-%d-%s%s.json", pageName, ts, readable, (sid != null ? "-session-" + sid : "")); - Path outFile = outDir.resolve(filename); - - Map envelope = new HashMap<>(); - envelope.put("student", student); - envelope.put("timestamp", ts); - envelope.put("timestampIso", readable); - envelope.put("page", pageName); - if (sid != null) { - envelope.put("sessionId", sid); - } - envelope.put("payload", payload); - - byte[] bytes = MAPPER.writerWithDefaultPrettyPrinter().writeValueAsBytes(envelope); - Files.write(outFile, bytes); - LOG.info("Wrote session JSON for {} page {} to {}", student, pageName, outFile); - return outFile; - } catch (IOException ex) { - LOG.warn("Unable to write session JSON for {} page {}: {}", student, pageName, ex.toString()); - return null; - } - } - - /** - * Convenience overload that accepts an int sessionId to avoid callers - * converting to String. Delegates to the string-based overload. - * - * @param student display name of the student - * @param pageName short page identifier - * @param payload arbitrary payload object - * @param explicitSessionId numeric session id - * @return written file path or null - */ - public static Path writeSessionJson(final String student, final String pageName, final Object payload, final int explicitSessionId) { - return writeSessionJson(student, pageName, payload, Integer.toString(explicitSessionId)); - } - - /** - * Backwards-compatible convenience method for callers that still have - * (codes,scores) arrays. It wraps them in a small Map and delegates to - * the main payload-based writer. - * - * @param student the student's display name - * @param pageName short page identifier (e.g. "Abacus") - * @param codes array of part codes to include in the payload - * @param scores array of scores corresponding to the codes - * @return path to the written JSON file, or null on failure - */ - public static Path writeSessionJson(final String student, final String pageName, final String[] codes, final int[] scores) { - com.studentgui.apphelpers.dto.AssessmentPayload payload = new com.studentgui.apphelpers.dto.AssessmentPayload(0, codes, scores); - return writeSessionJson(student, pageName, payload); - } -} +package com.studentgui.apphelpers; + +import java.io.IOException; +import java.nio.file.Files; +import java.nio.file.Path; +import java.time.Instant; +import java.time.ZoneId; +import java.time.format.DateTimeFormatter; +import java.util.HashMap; +import java.util.Map; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import com.fasterxml.jackson.databind.ObjectMapper; + +/** + * Helper to write per-session JSON exports for app pages. + */ +public final class SessionJsonWriter { + private static final Logger LOG = LoggerFactory.getLogger(SessionJsonWriter.class); + private static final ObjectMapper MAPPER = new ObjectMapper(); + + private SessionJsonWriter() {} + + /** + * Write a per-session JSON file into the student's StudentDataFiles folder. + * The filename will include a unix timestamp to ensure uniqueness per session. + * + * @param student display name of the student + * @param pageName short page identifier (e.g. "Abacus") + * @param payload arbitrary payload object to serialize (Map or POJO) + * @return the path to the written file, or null on failure + */ + public static Path writeSessionJson(final String student, final String pageName, final Object payload) { + return writeSessionJson(student, pageName, payload, null); + } + + /** + * Write a per-session JSON file and optionally include an explicit sessionId. + * If the explicit sessionId is null, this method will look for a "sessionId" + * entry inside the payload Map and use that if present. The envelope written + * to disk will include the sessionId when available. + * + * Filename format: {@code PageName--[-session-].json} + * + * @param student display name of the student + * @param pageName short page identifier (e.g. "Abacus") + * @param payload arbitrary payload object to serialize (Map or POJO) + * @param explicitSessionId optional session id to use in the envelope and filename + * @return the path to the written file, or null on failure + */ + public static Path writeSessionJson(final String student, final String pageName, final Object payload, final String explicitSessionId) { + if (student == null || student.trim().isEmpty() || pageName == null) { + return null; + } + try { + Path outDir = Helpers.studentCollectedDataDir(student); + Files.createDirectories(outDir); + long ts = Instant.now().toEpochMilli(); + // format for readability too + String readable = DateTimeFormatter.ofPattern("yyyyMMdd-HHmmssSSS").withZone(ZoneId.systemDefault()).format(Instant.ofEpochMilli(ts)); + + // Determine sessionId preference: explicit param first, then payload if it implements SessionPayload + String sid = explicitSessionId; + if (sid == null && payload instanceof com.studentgui.apphelpers.dto.SessionPayload) { + int s = ((com.studentgui.apphelpers.dto.SessionPayload) payload).getSessionId(); + if (s != 0) { + sid = Integer.toString(s); + } + } + + String filename = String.format("%s-%d-%s%s.json", pageName, ts, readable, (sid != null ? "-session-" + sid : "")); + Path outFile = outDir.resolve(filename); + + Map envelope = new HashMap<>(); + envelope.put("student", student); + envelope.put("timestamp", ts); + envelope.put("timestampIso", readable); + envelope.put("page", pageName); + if (sid != null) { + envelope.put("sessionId", sid); + } + envelope.put("payload", payload); + + byte[] bytes = MAPPER.writerWithDefaultPrettyPrinter().writeValueAsBytes(envelope); + Files.write(outFile, bytes); + LOG.info("Wrote session JSON for {} page {} to {}", student, pageName, outFile); + return outFile; + } catch (IOException ex) { + LOG.warn("Unable to write session JSON for {} page {}: {}", student, pageName, ex.toString()); + return null; + } + } + + /** + * Convenience overload that accepts an int sessionId to avoid callers + * converting to String. Delegates to the string-based overload. + * + * @param student display name of the student + * @param pageName short page identifier + * @param payload arbitrary payload object + * @param explicitSessionId numeric session id + * @return written file path or null + */ + public static Path writeSessionJson(final String student, final String pageName, final Object payload, final int explicitSessionId) { + return writeSessionJson(student, pageName, payload, Integer.toString(explicitSessionId)); + } + + /** + * Backwards-compatible convenience method for callers that still have + * (codes,scores) arrays. It wraps them in a small Map and delegates to + * the main payload-based writer. + * + * @param student the student's display name + * @param pageName short page identifier (e.g. "Abacus") + * @param codes array of part codes to include in the payload + * @param scores array of scores corresponding to the codes + * @return path to the written JSON file, or null on failure + */ + public static Path writeSessionJson(final String student, final String pageName, final String[] codes, final int[] scores) { + com.studentgui.apphelpers.dto.AssessmentPayload payload = new com.studentgui.apphelpers.dto.AssessmentPayload(0, codes, scores); + return writeSessionJson(student, pageName, payload); + } +} diff --git a/src/main/java/com/studentgui/apphelpers/Settings.java b/src/main/java/com/studentgui/apphelpers/Settings.java index 4accd23..dcf889a 100644 --- a/src/main/java/com/studentgui/apphelpers/Settings.java +++ b/src/main/java/com/studentgui/apphelpers/Settings.java @@ -1,57 +1,57 @@ -package com.studentgui.apphelpers; - -import java.io.IOException; -import java.io.InputStream; -import java.io.OutputStream; -import java.nio.file.Files; -import java.nio.file.Path; -import java.util.Properties; - -/** - * Lightweight settings persistence for simple key/value preferences. - */ -public final class Settings { - private static final Path SETTINGS_FILE = Helpers.APP_HOME.resolve("app.properties"); - private static final Properties props = new Properties(); - private static final org.slf4j.Logger LOG = org.slf4j.LoggerFactory.getLogger(Settings.class); - - static { - // load existing if present - try (InputStream in = Files.exists(SETTINGS_FILE) ? Files.newInputStream(SETTINGS_FILE) : null) { - if (in != null) { - props.load(in); - } - } catch (IOException ioe) { - LOG.debug("Could not load settings from {}", SETTINGS_FILE, ioe); - } - } - - private Settings() { throw new AssertionError(); } - - /** - * Get a persisted setting value or return a default when missing. - * - * @param key setting key - * @param def default value when key is absent - * @return stored value or default - */ - public static String get(final String key, final String def) { - return props.getProperty(key, def); - } - - /** - * Store a setting value and persist to disk immediately. - * - * @param key setting key - * @param value setting value (null treated as empty string) - */ - public static void put(final String key, final String value) { - props.setProperty(key, value == null ? "" : value); - // persist immediately - try (OutputStream out = Files.newOutputStream(SETTINGS_FILE)) { - props.store(out, "application settings"); - } catch (IOException ioe) { - LOG.debug("Could not persist settings to {}", SETTINGS_FILE, ioe); - } - } -} +package com.studentgui.apphelpers; + +import java.io.IOException; +import java.io.InputStream; +import java.io.OutputStream; +import java.nio.file.Files; +import java.nio.file.Path; +import java.util.Properties; + +/** + * Lightweight settings persistence for simple key/value preferences. + */ +public final class Settings { + private static final Path SETTINGS_FILE = Helpers.APP_HOME.resolve("app.properties"); + private static final Properties props = new Properties(); + private static final org.slf4j.Logger LOG = org.slf4j.LoggerFactory.getLogger(Settings.class); + + static { + // load existing if present + try (InputStream in = Files.exists(SETTINGS_FILE) ? Files.newInputStream(SETTINGS_FILE) : null) { + if (in != null) { + props.load(in); + } + } catch (IOException ioe) { + LOG.debug("Could not load settings from {}", SETTINGS_FILE, ioe); + } + } + + private Settings() { throw new AssertionError(); } + + /** + * Get a persisted setting value or return a default when missing. + * + * @param key setting key + * @param def default value when key is absent + * @return stored value or default + */ + public static String get(final String key, final String def) { + return props.getProperty(key, def); + } + + /** + * Store a setting value and persist to disk immediately. + * + * @param key setting key + * @param value setting value (null treated as empty string) + */ + public static void put(final String key, final String value) { + props.setProperty(key, value == null ? "" : value); + // persist immediately + try (OutputStream out = Files.newOutputStream(SETTINGS_FILE)) { + props.store(out, "application settings"); + } catch (IOException ioe) { + LOG.debug("Could not persist settings to {}", SETTINGS_FILE, ioe); + } + } +} diff --git a/src/main/java/com/studentgui/apphelpers/SqlGenerate.java b/src/main/java/com/studentgui/apphelpers/SqlGenerate.java index f3f4468..7bff5c6 100644 --- a/src/main/java/com/studentgui/apphelpers/SqlGenerate.java +++ b/src/main/java/com/studentgui/apphelpers/SqlGenerate.java @@ -1,190 +1,190 @@ -package com.studentgui.apphelpers; - -import java.io.IOException; -import java.nio.file.Files; -import java.nio.file.Path; -import java.sql.Connection; -import java.sql.DriverManager; -import java.sql.SQLException; -import java.sql.Statement; - -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; - -/** - * SQL schema generator for the normalized application database. - * - * This class ensures the SQLite database file exists and creates the - * canonical tables used by the application. Safe to call repeatedly on - * application startup. - */ -/** - * Utility responsible for creating/validating the on-disk SQLite database - * and canonical schema used by the application. Safe to call multiple times. - */ -public class SqlGenerate { - private static final Path DB = Helpers.DATABASE_PATH; - private static final Logger LOG = LoggerFactory.getLogger(SqlGenerate.class); - // Ported schema from Python appHelpers/sqlgenerate.py - private static final String[] SCHEMA = new String[] { - // Core student table - """ - CREATE TABLE IF NOT EXISTS Student ( - id INTEGER PRIMARY KEY AUTOINCREMENT, - name TEXT NOT NULL, - birthdate TEXT, - notes TEXT - ); - """, - // ProgressType - """ - CREATE TABLE IF NOT EXISTS ProgressType ( - id INTEGER PRIMARY KEY AUTOINCREMENT, - name TEXT NOT NULL UNIQUE, - description TEXT - ); - """, - // ProgressSession - """ - CREATE TABLE IF NOT EXISTS ProgressSession ( - id INTEGER PRIMARY KEY AUTOINCREMENT, - student_id INTEGER NOT NULL, - progress_type_id INTEGER NOT NULL, - date TEXT NOT NULL, - notes TEXT, - FOREIGN KEY(student_id) REFERENCES Student(id) ON DELETE CASCADE, - FOREIGN KEY(progress_type_id) REFERENCES ProgressType(id) ON DELETE CASCADE - ); - """, - // KeyboardingResult - """ - CREATE TABLE IF NOT EXISTS KeyboardingResult ( - id INTEGER PRIMARY KEY AUTOINCREMENT, - session_id INTEGER NOT NULL, - program TEXT NOT NULL, - topic TEXT NOT NULL, - speed INTEGER NOT NULL, - accuracy INTEGER NOT NULL, - FOREIGN KEY(session_id) REFERENCES ProgressSession(id) ON DELETE CASCADE - ); - """, - // TrialResult - """ - CREATE TABLE IF NOT EXISTS TrialResult ( - id INTEGER PRIMARY KEY AUTOINCREMENT, - session_id INTEGER NOT NULL, - task TEXT NOT NULL, - lesson TEXT, - session_label TEXT, - trial_number INTEGER NOT NULL, - score INTEGER, - FOREIGN KEY(session_id) REFERENCES ProgressSession(id) ON DELETE CASCADE - ); - """, - // TrialSessionSummary - """ - CREATE TABLE IF NOT EXISTS TrialSessionSummary ( - id INTEGER PRIMARY KEY AUTOINCREMENT, - session_id INTEGER NOT NULL UNIQUE, - median FLOAT, - notes TEXT, - FOREIGN KEY(session_id) REFERENCES ProgressSession(id) ON DELETE CASCADE - ); - """, - // AssessmentPart - """ - CREATE TABLE IF NOT EXISTS AssessmentPart ( - id INTEGER PRIMARY KEY AUTOINCREMENT, - progress_type_id INTEGER NOT NULL, - code TEXT NOT NULL, - description TEXT, - UNIQUE(progress_type_id, code), - FOREIGN KEY(progress_type_id) REFERENCES ProgressType(id) ON DELETE CASCADE - ); - """, - // AssessmentResult - """ - CREATE TABLE IF NOT EXISTS AssessmentResult ( - id INTEGER PRIMARY KEY AUTOINCREMENT, - session_id INTEGER NOT NULL, - part_id INTEGER NOT NULL, - score INTEGER, - FOREIGN KEY(session_id) REFERENCES ProgressSession(id) ON DELETE CASCADE, - FOREIGN KEY(part_id) REFERENCES AssessmentPart(id) ON DELETE CASCADE - ); - """ - , - // ContactLog details tied to a ProgressSession - """ - CREATE TABLE IF NOT EXISTS ContactLog ( - id INTEGER PRIMARY KEY AUTOINCREMENT, - session_id INTEGER NOT NULL, - student_name TEXT, - date TEXT, - guardian_name TEXT, - contact_method TEXT, - phone_number TEXT, - email_address TEXT, - contact_response TEXT, - contact_general TEXT, - contact_specific TEXT, - contact_notes TEXT, - FOREIGN KEY(session_id) REFERENCES ProgressSession(id) ON DELETE CASCADE - ); - """ - }; - - /** - * Ensure the database file and canonical schema exist. This method is idempotent - * and safe to call on application startup. It will create the parent folder - * for the DB file if necessary and apply the embedded SCHEMA statements. - */ - public static void initializeDatabase() { - try { - Path parent = DB.getParent(); - if (parent != null && !Files.exists(parent)) { - Files.createDirectories(parent); - } - if (Files.exists(DB) && Files.isDirectory(DB)) { - LOG.error("Path is a directory, cannot create DB file: {}", DB); - return; - } - if (Files.exists(DB)) { - LOG.info("Database already exists at {}", DB); - // even if the DB exists, ensure schema is present by connecting and executing schema statements - } - // create/connect to SQLite database file by opening a connection - String url = "jdbc:sqlite:" + DB.toString(); - try (Connection conn = DriverManager.getConnection(url)) { - if (conn != null) { - executeSchema(conn); - } - } - LOG.info("Database initialized/validated at {}", DB); - } catch (SQLException | IOException e) { - LOG.error("Error initializing database", e); - } - } - - /** - * Execute the SCHEMA statements on the provided connection. - * Extracted to make the schema application clearer and easier to test. - * - * @param conn established JDBC connection to the target SQLite DB - * @throws SQLException if applying any schema statement fails - */ - private static void executeSchema(final Connection conn) throws SQLException { - try (Statement st = conn.createStatement()) { - for (String sql : SCHEMA) { - st.execute(sql); - } - } - } - - /** - * Private constructor to prevent instantiation of this utility class. - */ - private SqlGenerate() { - throw new AssertionError("Not instantiable"); - } -} +package com.studentgui.apphelpers; + +import java.io.IOException; +import java.nio.file.Files; +import java.nio.file.Path; +import java.sql.Connection; +import java.sql.DriverManager; +import java.sql.SQLException; +import java.sql.Statement; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + * SQL schema generator for the normalized application database. + * + * This class ensures the SQLite database file exists and creates the + * canonical tables used by the application. Safe to call repeatedly on + * application startup. + */ +/** + * Utility responsible for creating/validating the on-disk SQLite database + * and canonical schema used by the application. Safe to call multiple times. + */ +public class SqlGenerate { + private static final Path DB = Helpers.DATABASE_PATH; + private static final Logger LOG = LoggerFactory.getLogger(SqlGenerate.class); + // Ported schema from Python appHelpers/sqlgenerate.py + private static final String[] SCHEMA = new String[] { + // Core student table + """ + CREATE TABLE IF NOT EXISTS Student ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + name TEXT NOT NULL, + birthdate TEXT, + notes TEXT + ); + """, + // ProgressType + """ + CREATE TABLE IF NOT EXISTS ProgressType ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + name TEXT NOT NULL UNIQUE, + description TEXT + ); + """, + // ProgressSession + """ + CREATE TABLE IF NOT EXISTS ProgressSession ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + student_id INTEGER NOT NULL, + progress_type_id INTEGER NOT NULL, + date TEXT NOT NULL, + notes TEXT, + FOREIGN KEY(student_id) REFERENCES Student(id) ON DELETE CASCADE, + FOREIGN KEY(progress_type_id) REFERENCES ProgressType(id) ON DELETE CASCADE + ); + """, + // KeyboardingResult + """ + CREATE TABLE IF NOT EXISTS KeyboardingResult ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + session_id INTEGER NOT NULL, + program TEXT NOT NULL, + topic TEXT NOT NULL, + speed INTEGER NOT NULL, + accuracy INTEGER NOT NULL, + FOREIGN KEY(session_id) REFERENCES ProgressSession(id) ON DELETE CASCADE + ); + """, + // TrialResult + """ + CREATE TABLE IF NOT EXISTS TrialResult ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + session_id INTEGER NOT NULL, + task TEXT NOT NULL, + lesson TEXT, + session_label TEXT, + trial_number INTEGER NOT NULL, + score INTEGER, + FOREIGN KEY(session_id) REFERENCES ProgressSession(id) ON DELETE CASCADE + ); + """, + // TrialSessionSummary + """ + CREATE TABLE IF NOT EXISTS TrialSessionSummary ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + session_id INTEGER NOT NULL UNIQUE, + median FLOAT, + notes TEXT, + FOREIGN KEY(session_id) REFERENCES ProgressSession(id) ON DELETE CASCADE + ); + """, + // AssessmentPart + """ + CREATE TABLE IF NOT EXISTS AssessmentPart ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + progress_type_id INTEGER NOT NULL, + code TEXT NOT NULL, + description TEXT, + UNIQUE(progress_type_id, code), + FOREIGN KEY(progress_type_id) REFERENCES ProgressType(id) ON DELETE CASCADE + ); + """, + // AssessmentResult + """ + CREATE TABLE IF NOT EXISTS AssessmentResult ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + session_id INTEGER NOT NULL, + part_id INTEGER NOT NULL, + score INTEGER, + FOREIGN KEY(session_id) REFERENCES ProgressSession(id) ON DELETE CASCADE, + FOREIGN KEY(part_id) REFERENCES AssessmentPart(id) ON DELETE CASCADE + ); + """ + , + // ContactLog details tied to a ProgressSession + """ + CREATE TABLE IF NOT EXISTS ContactLog ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + session_id INTEGER NOT NULL, + student_name TEXT, + date TEXT, + guardian_name TEXT, + contact_method TEXT, + phone_number TEXT, + email_address TEXT, + contact_response TEXT, + contact_general TEXT, + contact_specific TEXT, + contact_notes TEXT, + FOREIGN KEY(session_id) REFERENCES ProgressSession(id) ON DELETE CASCADE + ); + """ + }; + + /** + * Ensure the database file and canonical schema exist. This method is idempotent + * and safe to call on application startup. It will create the parent folder + * for the DB file if necessary and apply the embedded SCHEMA statements. + */ + public static void initializeDatabase() { + try { + Path parent = DB.getParent(); + if (parent != null && !Files.exists(parent)) { + Files.createDirectories(parent); + } + if (Files.exists(DB) && Files.isDirectory(DB)) { + LOG.error("Path is a directory, cannot create DB file: {}", DB); + return; + } + if (Files.exists(DB)) { + LOG.info("Database already exists at {}", DB); + // even if the DB exists, ensure schema is present by connecting and executing schema statements + } + // create/connect to SQLite database file by opening a connection + String url = "jdbc:sqlite:" + DB.toString(); + try (Connection conn = DriverManager.getConnection(url)) { + if (conn != null) { + executeSchema(conn); + } + } + LOG.info("Database initialized/validated at {}", DB); + } catch (SQLException | IOException e) { + LOG.error("Error initializing database", e); + } + } + + /** + * Execute the SCHEMA statements on the provided connection. + * Extracted to make the schema application clearer and easier to test. + * + * @param conn established JDBC connection to the target SQLite DB + * @throws SQLException if applying any schema statement fails + */ + private static void executeSchema(final Connection conn) throws SQLException { + try (Statement st = conn.createStatement()) { + for (String sql : SCHEMA) { + st.execute(sql); + } + } + } + + /** + * Private constructor to prevent instantiation of this utility class. + */ + private SqlGenerate() { + throw new AssertionError("Not instantiable"); + } +} diff --git a/src/main/java/com/studentgui/apphelpers/UiNotifier.java b/src/main/java/com/studentgui/apphelpers/UiNotifier.java index 9cdca94..f394c28 100644 --- a/src/main/java/com/studentgui/apphelpers/UiNotifier.java +++ b/src/main/java/com/studentgui/apphelpers/UiNotifier.java @@ -1,61 +1,61 @@ -package com.studentgui.apphelpers; - -import java.awt.BorderLayout; -import java.awt.Color; -import java.awt.Font; - -import javax.swing.JLabel; -import javax.swing.JWindow; -import javax.swing.SwingUtilities; - -/** - * Very small non-modal notification window for quick status messages. - * - * Lightweight utility used across pages to display transient, non-blocking - * notifications to the user. - */ -public class UiNotifier { - private static JWindow window; - private static final org.slf4j.Logger LOG = org.slf4j.LoggerFactory.getLogger(UiNotifier.class); - - /** - * Display a short, transient notification message on screen. - * - * @param message message text to display - */ - public static void show(final String message) { - SwingUtilities.invokeLater(() -> { - if (window != null) { - window.dispose(); - } - window = new JWindow(); - JLabel label = new JLabel(message); - label.setOpaque(true); - label.setBackground(new Color(0x22, 0x22, 0x22, 200)); - label.setForeground(Color.WHITE); - label.setFont(new Font(Font.SANS_SERIF, Font.PLAIN, 12)); - window.getContentPane().setLayout(new BorderLayout()); - window.getContentPane().add(label, BorderLayout.CENTER); - window.pack(); - window.setAlwaysOnTop(true); - window.setLocationRelativeTo(null); - window.setVisible(true); - // auto-hide after 2 seconds - new Thread(() -> { - try { Thread.sleep(2000); } - catch (InterruptedException ie) { LOG.debug("UiNotifier sleep interrupted", ie); Thread.currentThread().interrupt(); } - SwingUtilities.invokeLater(() -> { if (window != null) { window.dispose(); window = null; } }); - }).start(); - }); - } - - // Note: UiNotifier.show is intentionally lightweight and non-blocking; - // the implemented method above contains the behavior and JavaDoc. - - /** - * Private constructor to prevent instantiation. - */ - private UiNotifier() { - // utility only - } -} +package com.studentgui.apphelpers; + +import java.awt.BorderLayout; +import java.awt.Color; +import java.awt.Font; + +import javax.swing.JLabel; +import javax.swing.JWindow; +import javax.swing.SwingUtilities; + +/** + * Very small non-modal notification window for quick status messages. + * + * Lightweight utility used across pages to display transient, non-blocking + * notifications to the user. + */ +public class UiNotifier { + private static JWindow window; + private static final org.slf4j.Logger LOG = org.slf4j.LoggerFactory.getLogger(UiNotifier.class); + + /** + * Display a short, transient notification message on screen. + * + * @param message message text to display + */ + public static void show(final String message) { + SwingUtilities.invokeLater(() -> { + if (window != null) { + window.dispose(); + } + window = new JWindow(); + JLabel label = new JLabel(message); + label.setOpaque(true); + label.setBackground(new Color(0x22, 0x22, 0x22, 200)); + label.setForeground(Color.WHITE); + label.setFont(new Font(Font.SANS_SERIF, Font.PLAIN, 12)); + window.getContentPane().setLayout(new BorderLayout()); + window.getContentPane().add(label, BorderLayout.CENTER); + window.pack(); + window.setAlwaysOnTop(true); + window.setLocationRelativeTo(null); + window.setVisible(true); + // auto-hide after 2 seconds + new Thread(() -> { + try { Thread.sleep(2000); } + catch (InterruptedException ie) { LOG.debug("UiNotifier sleep interrupted", ie); Thread.currentThread().interrupt(); } + SwingUtilities.invokeLater(() -> { if (window != null) { window.dispose(); window = null; } }); + }).start(); + }); + } + + // Note: UiNotifier.show is intentionally lightweight and non-blocking; + // the implemented method above contains the behavior and JavaDoc. + + /** + * Private constructor to prevent instantiation. + */ + private UiNotifier() { + // utility only + } +} diff --git a/src/main/java/com/studentgui/apphelpers/dto/AssessmentPayload.java b/src/main/java/com/studentgui/apphelpers/dto/AssessmentPayload.java index c77a964..d1b236d 100644 --- a/src/main/java/com/studentgui/apphelpers/dto/AssessmentPayload.java +++ b/src/main/java/com/studentgui/apphelpers/dto/AssessmentPayload.java @@ -1,41 +1,41 @@ -package com.studentgui.apphelpers.dto; - -import java.util.Arrays; - -/** - * Typed payload for assessment-style pages (codes + scores). - */ -public class AssessmentPayload implements SessionPayload { - /** Database session id for this payload. */ - public int sessionId; - /** Array of part codes (e.g. "P1_1"). */ - public String[] codes; - /** Parallel array of integer scores. */ - public int[] scores; - - /** No-arg constructor for Jackson and tests. */ - public AssessmentPayload() {} - - /** - * Create an assessment payload. - * - * @param sessionIdParam numeric DB session id - * @param codesParam array of part codes - * @param scoresParam array of scores - */ - public AssessmentPayload(final int sessionIdParam, final String[] codesParam, final int[] scoresParam) { - this.sessionId = sessionIdParam; - this.codes = codesParam; - this.scores = scoresParam; - } - - @Override - public int getSessionId() { - return this.sessionId; - } - - @Override - public String toString() { - return "AssessmentPayload{sessionId=" + sessionId + ", codes=" + Arrays.toString(codes) + ", scores=" + Arrays.toString(scores) + "}"; - } -} +package com.studentgui.apphelpers.dto; + +import java.util.Arrays; + +/** + * Typed payload for assessment-style pages (codes + scores). + */ +public class AssessmentPayload implements SessionPayload { + /** Database session id for this payload. */ + public int sessionId; + /** Array of part codes (e.g. "P1_1"). */ + public String[] codes; + /** Parallel array of integer scores. */ + public int[] scores; + + /** No-arg constructor for Jackson and tests. */ + public AssessmentPayload() {} + + /** + * Create an assessment payload. + * + * @param sessionIdParam numeric DB session id + * @param codesParam array of part codes + * @param scoresParam array of scores + */ + public AssessmentPayload(final int sessionIdParam, final String[] codesParam, final int[] scoresParam) { + this.sessionId = sessionIdParam; + this.codes = codesParam; + this.scores = scoresParam; + } + + @Override + public int getSessionId() { + return this.sessionId; + } + + @Override + public String toString() { + return "AssessmentPayload{sessionId=" + sessionId + ", codes=" + Arrays.toString(codes) + ", scores=" + Arrays.toString(scores) + "}"; + } +} diff --git a/src/main/java/com/studentgui/apphelpers/dto/ContactPayload.java b/src/main/java/com/studentgui/apphelpers/dto/ContactPayload.java index 926c402..7a21d92 100644 --- a/src/main/java/com/studentgui/apphelpers/dto/ContactPayload.java +++ b/src/main/java/com/studentgui/apphelpers/dto/ContactPayload.java @@ -1,56 +1,56 @@ -package com.studentgui.apphelpers.dto; - -/** - * Typed payload for contact log entries. - */ -public class ContactPayload implements SessionPayload { - /** Database session id. */ - public int sessionId; - /** Guardian/parent name. */ - public String guardian; - /** Method of contact (Phone/Email/etc). */ - public String method; - /** Phone number. */ - public String phone; - /** Email address. */ - public String email; - /** Brief response summary. */ - public String response; - /** High-level general notes. */ - public String general; - /** Specific action items or points. */ - public String specific; - /** Full notes text. */ - public String notes; - - /** No-arg constructor for Jackson. */ - public ContactPayload() {} - - /** - * Create a contact payload. - * - * @param sessionIdParam database session id - * @param guardianParam guardian/parent name - * @param methodParam method of contact (Phone/Email/etc) - * @param phoneParam phone number - * @param emailParam email address - * @param responseParam brief response summary - * @param generalParam high-level general notes - * @param specificParam specific action items or points - * @param notesParam full notes text - */ - public ContactPayload(final int sessionIdParam, final String guardianParam, final String methodParam, final String phoneParam, final String emailParam, final String responseParam, final String generalParam, final String specificParam, final String notesParam) { - this.sessionId = sessionIdParam; - this.guardian = guardianParam; - this.method = methodParam; - this.phone = phoneParam; - this.email = emailParam; - this.response = responseParam; - this.general = generalParam; - this.specific = specificParam; - this.notes = notesParam; - } - - @Override - public int getSessionId() { return this.sessionId; } -} +package com.studentgui.apphelpers.dto; + +/** + * Typed payload for contact log entries. + */ +public class ContactPayload implements SessionPayload { + /** Database session id. */ + public int sessionId; + /** Guardian/parent name. */ + public String guardian; + /** Method of contact (Phone/Email/etc). */ + public String method; + /** Phone number. */ + public String phone; + /** Email address. */ + public String email; + /** Brief response summary. */ + public String response; + /** High-level general notes. */ + public String general; + /** Specific action items or points. */ + public String specific; + /** Full notes text. */ + public String notes; + + /** No-arg constructor for Jackson. */ + public ContactPayload() {} + + /** + * Create a contact payload. + * + * @param sessionIdParam database session id + * @param guardianParam guardian/parent name + * @param methodParam method of contact (Phone/Email/etc) + * @param phoneParam phone number + * @param emailParam email address + * @param responseParam brief response summary + * @param generalParam high-level general notes + * @param specificParam specific action items or points + * @param notesParam full notes text + */ + public ContactPayload(final int sessionIdParam, final String guardianParam, final String methodParam, final String phoneParam, final String emailParam, final String responseParam, final String generalParam, final String specificParam, final String notesParam) { + this.sessionId = sessionIdParam; + this.guardian = guardianParam; + this.method = methodParam; + this.phone = phoneParam; + this.email = emailParam; + this.response = responseParam; + this.general = generalParam; + this.specific = specificParam; + this.notes = notesParam; + } + + @Override + public int getSessionId() { return this.sessionId; } +} diff --git a/src/main/java/com/studentgui/apphelpers/dto/KeyboardingPayload.java b/src/main/java/com/studentgui/apphelpers/dto/KeyboardingPayload.java index 245b63b..842e203 100644 --- a/src/main/java/com/studentgui/apphelpers/dto/KeyboardingPayload.java +++ b/src/main/java/com/studentgui/apphelpers/dto/KeyboardingPayload.java @@ -1,40 +1,40 @@ -package com.studentgui.apphelpers.dto; - -/** - * Typed payload for Keyboarding page. - */ -public class KeyboardingPayload implements SessionPayload { - /** Database session id. */ - public int sessionId; - /** Program or curriculum name. */ - public String program; - /** Topic or lesson name. */ - public String topic; - /** Speed in WPM. */ - public int speed; - /** Accuracy percentage. */ - public int accuracy; - - /** No-arg constructor for Jackson. */ - public KeyboardingPayload() {} - - /** - * Create keyboarding payload. - * - * @param sessionIdParam DB session id - * @param programParam program name - * @param topicParam topic name - * @param speedParam words per minute - * @param accuracyParam percent accuracy - */ - public KeyboardingPayload(final int sessionIdParam, final String programParam, final String topicParam, final int speedParam, final int accuracyParam) { - this.sessionId = sessionIdParam; - this.program = programParam; - this.topic = topicParam; - this.speed = speedParam; - this.accuracy = accuracyParam; - } - - @Override - public int getSessionId() { return this.sessionId; } -} +package com.studentgui.apphelpers.dto; + +/** + * Typed payload for Keyboarding page. + */ +public class KeyboardingPayload implements SessionPayload { + /** Database session id. */ + public int sessionId; + /** Program or curriculum name. */ + public String program; + /** Topic or lesson name. */ + public String topic; + /** Speed in WPM. */ + public int speed; + /** Accuracy percentage. */ + public int accuracy; + + /** No-arg constructor for Jackson. */ + public KeyboardingPayload() {} + + /** + * Create keyboarding payload. + * + * @param sessionIdParam DB session id + * @param programParam program name + * @param topicParam topic name + * @param speedParam words per minute + * @param accuracyParam percent accuracy + */ + public KeyboardingPayload(final int sessionIdParam, final String programParam, final String topicParam, final int speedParam, final int accuracyParam) { + this.sessionId = sessionIdParam; + this.program = programParam; + this.topic = topicParam; + this.speed = speedParam; + this.accuracy = accuracyParam; + } + + @Override + public int getSessionId() { return this.sessionId; } +} diff --git a/src/main/java/com/studentgui/apphelpers/dto/NotesPayload.java b/src/main/java/com/studentgui/apphelpers/dto/NotesPayload.java index 4e08111..fb7cedd 100644 --- a/src/main/java/com/studentgui/apphelpers/dto/NotesPayload.java +++ b/src/main/java/com/studentgui/apphelpers/dto/NotesPayload.java @@ -1,28 +1,28 @@ -package com.studentgui.apphelpers.dto; - -/** - * Typed payload for freeform notes pages. - */ -public class NotesPayload implements SessionPayload { - /** Database session id. */ - public int sessionId; - /** The freeform notes text. */ - public String notes; - - /** No-arg constructor for Jackson. */ - public NotesPayload() {} - - /** - * Create a notes payload. - * - * @param sessionIdParam DB session id - * @param notesParam freeform notes - */ - public NotesPayload(final int sessionIdParam, final String notesParam) { - this.sessionId = sessionIdParam; - this.notes = notesParam; - } - - @Override - public int getSessionId() { return this.sessionId; } -} +package com.studentgui.apphelpers.dto; + +/** + * Typed payload for freeform notes pages. + */ +public class NotesPayload implements SessionPayload { + /** Database session id. */ + public int sessionId; + /** The freeform notes text. */ + public String notes; + + /** No-arg constructor for Jackson. */ + public NotesPayload() {} + + /** + * Create a notes payload. + * + * @param sessionIdParam DB session id + * @param notesParam freeform notes + */ + public NotesPayload(final int sessionIdParam, final String notesParam) { + this.sessionId = sessionIdParam; + this.notes = notesParam; + } + + @Override + public int getSessionId() { return this.sessionId; } +} diff --git a/src/main/java/com/studentgui/apphelpers/dto/SessionPayload.java b/src/main/java/com/studentgui/apphelpers/dto/SessionPayload.java index e27069a..874f8a6 100644 --- a/src/main/java/com/studentgui/apphelpers/dto/SessionPayload.java +++ b/src/main/java/com/studentgui/apphelpers/dto/SessionPayload.java @@ -1,13 +1,13 @@ -package com.studentgui.apphelpers.dto; - -/** - * Common interface for session-scoped payloads that carry a DB session id. - */ -public interface SessionPayload { - /** - * Return the database session id associated with this payload. - * - * @return the database session id for this payload (may be 0 when unknown) - */ - int getSessionId(); -} +package com.studentgui.apphelpers.dto; + +/** + * Common interface for session-scoped payloads that carry a DB session id. + */ +public interface SessionPayload { + /** + * Return the database session id associated with this payload. + * + * @return the database session id for this payload (may be 0 when unknown) + */ + int getSessionId(); +} diff --git a/src/main/java/com/studentgui/apppages/Abacus.java b/src/main/java/com/studentgui/apppages/Abacus.java index 46bba7e..5cb861b 100644 --- a/src/main/java/com/studentgui/apppages/Abacus.java +++ b/src/main/java/com/studentgui/apppages/Abacus.java @@ -1,409 +1,442 @@ -package com.studentgui.apppages; - -import java.awt.BorderLayout; -import java.awt.Font; -import java.awt.GridBagConstraints; -import java.awt.GridBagLayout; -import java.awt.Insets; -import java.awt.event.ActionEvent; -import java.awt.event.KeyEvent; -import java.sql.SQLException; -import java.time.LocalDate; - -import javax.swing.JButton; -import javax.swing.JLabel; -import javax.swing.JOptionPane; -import javax.swing.JPanel; -import javax.swing.JScrollPane; -import javax.swing.SwingUtilities; - -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; - -/** - * Abacus skills progression UI page. - *

- * Presents a scrollable list of abacus-related skill input fields for a - * particular student and date. Values entered here are persisted via the - * centralized database helper into the normalized schema and can be plotted - * using the shared {@link JLineGraph} component. - *

- */ -public class Abacus extends JPanel implements com.studentgui.app.DateChangeListener, com.studentgui.app.StudentChangeListener { - private static final Logger LOG = LoggerFactory.getLogger(Abacus.class); - - /** Array of input components for each skill. */ - private final com.studentgui.uicomp.PhaseScoreField[] skillFields; - /** Canonical list of abacus assessment parts: code and display label. */ - private final String[][] parts; - /** Shared graph component used to visualize recent results. */ - private final JLineGraph lineGraph; // Reference to the JLineGraph instance - /** Selected student display name (may be null). */ - private String studentNameParam; - /** Session date associated with persisted progress. */ - private LocalDate dateParam; - /** - * Title label shown at the top of the page. - */ - private JLabel titleLabel; - /** - * Base title text used when rendering the page header (date suffixes are appended). - */ - private final String baseTitle = "Abacus Skills Progression"; - - /** - * Construct the Abacus page for the given student and session date. - * - * @param studentName the selected student's display name (may be null before selection) - * @param date the date to associate with created progress sessions - * @param lineGraph the shared graph component used to visualize results - */ - public Abacus(final String studentName, final LocalDate date, final JLineGraph lineGraph) { - this.studentNameParam = (studentName == null || studentName.trim().isEmpty()) ? com.studentgui.apphelpers.Helpers.defaultStudent() : studentName; - this.dateParam = date; - this.lineGraph = lineGraph; // Use the passed in graph instance - setLayout(new BorderLayout()); - - // Initialize skills array and layout using canonical abacus parts - this.parts = new String[][]{ - {"P1_1","1.1 Setting Numbers"},{"P1_2","1.2 Clearing Beads"},{"P1_3","1.3 Place Value"},{"P1_4","1.4 Vocabulary"}, - {"P2_1","2.1 Addition of Single Digit Numbers"},{"P2_2","2.2 Direct Addition"},{"P2_3","2.3 Indirect Addition"}, - {"P3_1","3.1 Subtraction of Single Digit Numbers"},{"P3_2","3.2 Direct Subtraction"},{"P3_3","3.3 Indirect Subtraction"}, - {"P4_1","4.1 Multiplication – 2+ Digit Multiplicand 1-Digit Multiplier"},{"P4_2","4.2 Multiplication – 2+ Digit Multiplicand AND Multiplier"}, - {"P5_1","5.1 Division – 2+ Digit Dividend 1-Digit Divisor"},{"P5_2","5.2 Division – 2+ Digit Dividend AND 1 Digit Divisor"}, - {"P6_1","6.1 Addition of Decimals"},{"P6_2","6.2 Subtraction of Decimals"},{"P6_3","6.3 Multiplication of Decimals"},{"P6_4","6.4 Division of Decimals"}, - {"P7_1","7.1 Addition of Fractions"},{"P7_2","7.2 Subtraction of Fractions"},{"P7_3","7.3 Multiplication of Fractions"},{"P7_4","7.4 Division of Fractions"}, - {"P8_1","8.1 Percent"},{"P8_2","8.2 Square Root"} - }; - - // Panel for data entry - JPanel dataEntryPanel = new JPanel(); - dataEntryPanel.setLayout(new GridBagLayout()); - JPanel view = new JPanel(new BorderLayout()); - view.add(dataEntryPanel, BorderLayout.NORTH); - view.setBorder(javax.swing.BorderFactory.createEmptyBorder(20,20,20,20)); - JScrollPane dataEntryScrollPane = new JScrollPane(view); - dataEntryScrollPane.setVerticalScrollBarPolicy(JScrollPane.VERTICAL_SCROLLBAR_AS_NEEDED); - dataEntryScrollPane.setHorizontalScrollBarPolicy(JScrollPane.HORIZONTAL_SCROLLBAR_NEVER); - dataEntryScrollPane.getAccessibleContext().setAccessibleName("Abacus data entry scroll pane"); - - GridBagConstraints gbc = new GridBagConstraints(); - gbc.insets = new Insets(2, 2, 2, 2); - gbc.fill = GridBagConstraints.HORIZONTAL; - gbc.weightx = 1.0; - gbc.weighty = 0.0; - - this.titleLabel = new JLabel(baseTitle); - this.titleLabel.setFont(this.titleLabel.getFont().deriveFont(Font.BOLD, 16)); - gbc.gridx = 0; - gbc.gridy = 0; - gbc.gridwidth = GridBagConstraints.REMAINDER; - dataEntryPanel.add(titleLabel, gbc); - - gbc.gridy = 1; - gbc.gridwidth = GridBagConstraints.REMAINDER; - gbc.ipady = 20; - dataEntryPanel.add(new JPanel(), gbc); - - // visual spacing controlled by PhaseScoreField and layout - - String[] labels = java.util.Arrays.stream(this.parts).map(x->x[1]).toArray(String[]::new); - int maxPx = com.studentgui.uicomp.PhaseScoreField.computeMaxLabelPixelWidth(titleLabel.getFont(), labels); - com.studentgui.uicomp.PhaseScoreField.setGlobalLabelWidth(Math.min(320, Math.max(140, maxPx + 50))); - skillFields = new com.studentgui.uicomp.PhaseScoreField[this.parts.length]; - for (int i = 0; i < this.parts.length; i++) { - gbc.gridy = i + 2; - gbc.gridx = 0; - gbc.gridwidth = 1; - com.studentgui.uicomp.PhaseScoreField field = new com.studentgui.uicomp.PhaseScoreField(this.parts[i][1], 0); - field.setName("abacus_" + this.parts[i][0]); - field.getAccessibleContext().setAccessibleName(this.parts[i][1]); - field.setToolTipText("Enter a numeric score for " + this.parts[i][1]); - gbc.gridx = 0; gbc.gridwidth = 2; gbc.insets = new Insets(2, 2, 2, 2); - dataEntryPanel.add(field, gbc); - skillFields[i] = field; - gbc.gridx = 2; gbc.gridwidth = 1; gbc.insets = new Insets(2, 0, 2, 2); - dataEntryPanel.add(new JPanel(), gbc); - } - - gbc.gridy = this.parts.length + 3; - gbc.gridx = 0; - gbc.gridwidth = GridBagConstraints.REMAINDER; - gbc.weighty = 1.0; - dataEntryPanel.add(new JPanel(), gbc); - - // Place Submit and Open Latest side-by-side with IOS-like height - gbc.gridy = this.parts.length + 4; - gbc.weighty = 0.0; - gbc.gridx = 0; - gbc.gridwidth = 1; - JButton submitDataButton = new JButton("Submit Data"); - submitDataButton.setPreferredSize(new java.awt.Dimension(0, 32)); - submitDataButton.addActionListener((ActionEvent e) -> { submitData(); refreshGraph(); }); - submitDataButton.setMnemonic(KeyEvent.VK_S); - submitDataButton.setToolTipText("Save Abacus scores for the selected student (Alt+S)"); - submitDataButton.getAccessibleContext().setAccessibleName("Submit Abacus Data"); - dataEntryPanel.add(submitDataButton, gbc); - - gbc.gridx = 1; - JButton openLatestBtn = new JButton("Open Latest Plot"); - openLatestBtn.setPreferredSize(new java.awt.Dimension(0, 32)); - openLatestBtn.addActionListener((ActionEvent e) -> { - java.nio.file.Path p = com.studentgui.apphelpers.Helpers.latestPlotPath(this.studentNameParam, "Abacus"); - if (p == null) { - com.studentgui.apphelpers.UiNotifier.show("No Abacus plot found for student"); - } else { - try { - java.awt.Desktop.getDesktop().open(p.toFile()); - } catch (java.io.IOException | UnsupportedOperationException | SecurityException ex) { - com.studentgui.apphelpers.UiNotifier.show("Unable to open plot: " + p.getFileName().toString()); - } - } - }); - dataEntryPanel.add(openLatestBtn, gbc); - - gbc.gridx = 2; gbc.gridwidth = GridBagConstraints.REMAINDER; - dataEntryPanel.add(new JPanel(), gbc); - - add(dataEntryScrollPane, BorderLayout.CENTER); - - // Add existing graph reference - add(lineGraph, BorderLayout.SOUTH); - - SwingUtilities.invokeLater(() -> { - dataEntryPanel.setPreferredSize(dataEntryPanel.getPreferredSize()); - updateTitleDate(); - revalidate(); - }); - - // Ensure application folders and DB schema exist before DB operations - com.studentgui.apphelpers.Helpers.createFolderHierarchy(); - initDatabase(); - refreshGraph(); - } - - /** - * Ensure the canonical progress-type and assessment parts for Abacus exist - * in the normalized database schema. Safe to call multiple times. - */ - private void initDatabase() { - try { - int ptId = com.studentgui.apphelpers.Database.getOrCreateProgressType("Abacus"); - // Use the canonical part codes declared on this page so parts are created - // with the expected codes like "P1_1", "P1_2", ... - String[] codes = new String[this.parts.length]; - for (int i = 0; i < this.parts.length; i++) { - codes[i] = this.parts[i][0]; - } - com.studentgui.apphelpers.Database.ensureAssessmentParts(ptId, codes); - try { - com.studentgui.apphelpers.Database.cleanupAssessmentParts(ptId, codes); - } catch (SQLException se) { - LOG.warn("Could not cleanup legacy parts for Abacus", se); - } - } catch (SQLException e) { - LOG.error("SQL error initializing Abacus parts", e); - } - } - - /** - * Read input fields, validate numeric input, and persist the values as a - * new progress session for the selected student. - */ - private void submitData() { - if (studentNameParam == null || studentNameParam.trim().isEmpty()) { - JOptionPane.showMessageDialog(this, "Please select a student before submitting Abacus data.", "Missing student", JOptionPane.WARNING_MESSAGE); - return; - } - - try { - int studentId = com.studentgui.apphelpers.Database.getOrCreateStudent(studentNameParam); - int ptId = com.studentgui.apphelpers.Database.getOrCreateProgressType("Abacus"); - int sessionId = com.studentgui.apphelpers.Database.createProgressSession(studentId, ptId, dateParam); - - String[] codes = new String[this.parts.length]; - int[] scores = new int[this.parts.length]; - for (int i = 0; i < this.parts.length; i++) { - codes[i] = this.parts[i][0]; - scores[i] = skillFields[i].getValue(); - } - com.studentgui.apphelpers.Database.insertAssessmentResults(sessionId, ptId, codes, scores); - LOG.info("Data submitted successfully via normalized schema."); - com.studentgui.apphelpers.UiNotifier.show("Abacus data saved."); - // Also persist this session as a JSON file in the student's folder (timestamped per-session) - com.studentgui.apphelpers.dto.AssessmentPayload payload = new com.studentgui.apphelpers.dto.AssessmentPayload(sessionId, codes, scores); - java.nio.file.Path jsonOut = com.studentgui.apphelpers.SessionJsonWriter.writeSessionJson(this.studentNameParam, "Abacus", payload, sessionId); - if (jsonOut == null) { - LOG.warn("Unable to save Abacus session JSON for sessionId={}", sessionId); - } - // Generate per-phase PNGs (time-series) and a markdown report for this session - try { - java.nio.file.Path plotsOut = com.studentgui.apphelpers.Helpers.studentPlotsDir(this.studentNameParam); - java.nio.file.Path reportsOut = com.studentgui.apphelpers.Helpers.studentReportsDir(this.studentNameParam); - java.nio.file.Files.createDirectories(plotsOut); - java.nio.file.Files.createDirectories(reportsOut); - java.time.format.DateTimeFormatter df = java.time.format.DateTimeFormatter.ISO_DATE; - String dateStr = (this.dateParam != null ? this.dateParam.format(df) : java.time.LocalDate.now().toString()); - String baseName = "Abacus-" + sessionId + "-" + dateStr; - - // Fetch recent dated sessions (oldest first) to build time-series plots. - com.studentgui.apphelpers.Database.ResultsWithDates rwd = com.studentgui.apphelpers.Database.fetchLatestAssessmentResultsWithDates(this.studentNameParam, "Abacus", Integer.MAX_VALUE); - - java.util.Map groups = null; - if (rwd != null && rwd.rows != null && !rwd.rows.isEmpty()) { - // Build human-friendly labels from this.parts and render time-series grouped charts - String[] labels = new String[this.parts.length]; - for (int i = 0; i < this.parts.length; i++) { - labels[i] = this.parts[i][1]; - } - lineGraph.updateWithGroupedDataByDate(rwd.dates, rwd.rows, codes, labels); - // Persist each group as a PNG (time-series image) - groups = lineGraph.saveGroupedCharts(plotsOut, baseName, 1000, 240); - // Use the most-recent session date for the report header if available - java.time.LocalDate headerDate = rwd.dates.get(rwd.dates.size() - 1); - dateStr = headerDate.format(df); - } else { - // Fallback: render only the latest session snapshot - java.util.List> rows = new java.util.ArrayList<>(); - java.util.List latest = new java.util.ArrayList<>(); - for (int v : scores) { - latest.add(v); - } - rows.add(latest); - lineGraph.updateWithGroupedData(rows, codes); - groups = lineGraph.saveGroupedCharts(plotsOut, baseName, 1000, 240); - } - - // Generate markdown report - if (groups == null) { - groups = new java.util.LinkedHashMap<>(); - } - StringBuilder md = new StringBuilder(); - md.append("# ").append(this.studentNameParam == null ? "Unknown Student" : this.studentNameParam).append(" - ").append(dateStr).append("\n\n"); - for (java.util.Map.Entry e : groups.entrySet()) { - md.append("## ").append(e.getKey()).append("\n\n"); - md.append("![](./").append(e.getValue().getFileName().toString()).append(")\n\n"); - } - java.nio.file.Path mdFile = reportsOut.resolve(baseName + ".md"); - // images live in ../plots relative to reports - String mdText = md.toString().replace("![](./", "![](../plots/"); - java.nio.file.Files.writeString(mdFile, mdText, java.nio.charset.StandardCharsets.UTF_8); - LOG.info("Wrote Abacus session report {} with {} group images", mdFile, groups.size()); - // Also produce a simple HTML report that embeds the PNGs and - // shows a scrollable legend under each plot. - try { - String[] palette = new String[] {"#1b9e77","#d95f02","#7570b3","#e7298a","#66a61e","#e6ab02","#a6761d","#666666"}; - - // Build a map of group -> list of part indexes to recreate legend order - java.util.LinkedHashMap> groupsIdx = new java.util.LinkedHashMap<>(); - for (int i = 0; i < codes.length; i++) { - String code = codes[i]; - String grp = code != null && code.contains("_") ? code.split("_")[0] : code; - groupsIdx.computeIfAbsent(grp, k -> new java.util.ArrayList<>()).add(i); - } - - StringBuilder html = new StringBuilder(); - html.append("\n"); - html.append(this.studentNameParam == null ? "Student Report" : this.studentNameParam).append(" - ").append(dateStr); - html.append(""); - html.append(""); - html.append(""); - html.append("

").append(this.studentNameParam == null ? "Unknown Student" : this.studentNameParam).append(" - ").append(dateStr).append("

"); - - for (java.util.Map.Entry e2 : groups.entrySet()) { - String grp = e2.getKey(); - String imgName = e2.getValue().getFileName().toString(); - html.append("

").append(grp).append("

"); - html.append("
\"").append(grp).append("\"
"); - - // legend for this group - java.util.List idxs = groupsIdx.getOrDefault(grp, new java.util.ArrayList<>()); - html.append("
"); - for (int s = 0; s < idxs.size(); s++) { - int idx = idxs.get(s); - String code = codes[idx]; - String human = this.parts[idx][1]; - String seriesName = code + " - " + human; - String color = palette[s % palette.length]; - html.append("
"); - html.append("
").append(seriesName).append("
"); - } - html.append("
"); - } - - html.append(""); - java.nio.file.Path htmlFile = reportsOut.resolve(baseName + ".html"); - // adjust image src to point to ../plots - String htmlStr = html.toString().replace("src=\"./", "src=\"../plots/"); - java.nio.file.Files.writeString(htmlFile, htmlStr, java.nio.charset.StandardCharsets.UTF_8); - LOG.info("Wrote Abacus HTML session report {}", htmlFile); - } catch (java.io.IOException ioex) { - LOG.warn("Unable to write HTML report: {}", ioex.toString()); - } - } catch (java.io.IOException | SQLException ex) { - LOG.warn("Unable to save Abacus per-phase plots or markdown report: {}", ex.toString()); - } - } catch (SQLException e) { - LOG.error("SQL error in submitData", e); - JOptionPane.showMessageDialog(this, "Database error saving Abacus data: " + e.getMessage(), "Database error", JOptionPane.ERROR_MESSAGE); - } - } - - /** - * Load recent assessment sessions for the selected student and update the - * shared {@link JLineGraph} with the returned metric series. - */ - private void refreshGraph() { - try { - com.studentgui.apphelpers.Database.ResultsWithDates rwd = com.studentgui.apphelpers.Database.fetchLatestAssessmentResultsWithDates(studentNameParam, "Abacus", Integer.MAX_VALUE); - String[] codes = new String[this.parts.length]; - for (int i = 0; i < this.parts.length; i++) { - codes[i] = this.parts[i][0]; - } - if (rwd != null && rwd.rows != null && !rwd.rows.isEmpty()) { - // Use the date-aware grouped plotter so X axis is dates and each - // skill within a phase is a separate line series. - String[] labels = new String[this.parts.length]; - for (int i = 0; i < this.parts.length; i++) { - labels[i] = this.parts[i][1]; - } - lineGraph.updateWithGroupedDataByDate(rwd.dates, rwd.rows, codes, labels); - LOG.debug("Graph updated with {} dated sessions", rwd.rows.size()); - } else { - LOG.info("No data to plot; showing grouped placeholders."); - lineGraph.showEmptyGrouped(codes); - } - } catch (SQLException e) { - LOG.error("SQL error refreshing graph", e); - } - } - @Override - public void dateChanged(final LocalDate newDate) { - this.dateParam = newDate; - // When the global date changes, update the graph to reflect any - // date-related logic (most refreshGraph implementations load - // recent sessions independent of the selected session date, but - // updating here keeps the saved date in sync for future submits). - SwingUtilities.invokeLater(this::refreshGraph); - } - - @Override - public void studentChanged(final String newStudent) { - this.studentNameParam = newStudent; - SwingUtilities.invokeLater(() -> { - refreshGraph(); - updateTitleDate(); - }); - } - - private void updateTitleDate() { - try { - String dateStr = this.dateParam != null ? this.dateParam.toString() : java.time.LocalDate.now().toString(); - this.titleLabel.setText(baseTitle + " - " + dateStr); - } catch (Exception ex) { - this.titleLabel.setText(baseTitle); - } - } - - -} +package com.studentgui.apppages; + +import java.awt.BorderLayout; +import java.awt.Font; +import java.awt.GridBagConstraints; +import java.awt.GridBagLayout; +import java.awt.Insets; +import java.awt.event.ActionEvent; +import java.awt.event.KeyEvent; +import java.sql.SQLException; +import java.time.LocalDate; + +import javax.swing.JButton; +import javax.swing.JLabel; +import javax.swing.JOptionPane; +import javax.swing.JPanel; +import javax.swing.JScrollPane; +import javax.swing.SwingUtilities; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + * Abacus computational skills assessment page. + * + *

Provides a structured interface for evaluating student proficiency with the Cranmer + * Abacus across 22 standardized skills organized into 8 progressive competency phases:

+ * + *
    + *
  • Phase 1 (P1_1–P1_4): Foundational bead manipulation (setting, clearing, place value, vocabulary)
  • + *
  • Phase 2 (P2_1–P2_3): Single-digit addition (direct and indirect methods)
  • + *
  • Phase 3 (P3_1–P3_3): Single-digit subtraction (direct and indirect methods)
  • + *
  • Phase 4 (P4_1–P4_2): Multiplication with multi-digit operands
  • + *
  • Phase 5 (P5_1–P5_2): Division with multi-digit operands
  • + *
  • Phase 6 (P6_1–P6_4): Decimal arithmetic (all four operations)
  • + *
  • Phase 7 (P7_1–P7_4): Fraction arithmetic (all four operations)
  • + *
  • Phase 8 (P8_1–P8_2): Advanced operations (percentages, square roots)
  • + *
+ * + *

Data Persistence and Export:

+ *
    + *
  • Skill scores are captured via {@link com.studentgui.uicomp.PhaseScoreField} components (integer 0–4 typical)
  • + *
  • Submit button persists values to normalized schema using {@link com.studentgui.apphelpers.Database#insertAssessmentResults}
  • + *
  • Session data exported to timestamped JSON in {@code StudentDataFiles//Sessions/Abacus/}
  • + *
  • Per-phase time-series plots generated and saved to {@code plots/} directory
  • + *
  • Comprehensive Markdown and HTML reports generated with embedded phase plots and color-coded legends
  • + *
+ * + *

Report Artifacts:

+ *
    + *
  • JSON export: {@code Abacus--.json} with session envelope
  • + *
  • Phase group plots: {@code Abacus---P.png} (8 PNG images)
  • + *
  • Markdown report: {@code reports/Abacus--.md} with relative image links
  • + *
  • HTML report: {@code reports/Abacus--.html} with inline styles and legends
  • + *
+ * + *

The shared {@link JLineGraph} visualizes recent session trends, grouping skills by phase prefix + * to maintain chart readability. Implements {@link com.studentgui.app.DateChangeListener} and + * {@link com.studentgui.app.StudentChangeListener} for dynamic updates when global selections change.

+ * + * @see com.studentgui.apphelpers.Database + * @see JLineGraph + * @see com.studentgui.uicomp.PhaseScoreField + */ +public class Abacus extends JPanel implements com.studentgui.app.DateChangeListener, com.studentgui.app.StudentChangeListener { + private static final Logger LOG = LoggerFactory.getLogger(Abacus.class); + + /** Array of input components for each skill. */ + private final com.studentgui.uicomp.PhaseScoreField[] skillFields; + /** Canonical list of abacus assessment parts: code and display label. */ + private final String[][] parts; + /** Shared graph component used to visualize recent results. */ + private final JLineGraph lineGraph; // Reference to the JLineGraph instance + /** Selected student display name (may be null). */ + private String studentNameParam; + /** Session date associated with persisted progress. */ + private LocalDate dateParam; + /** + * Title label shown at the top of the page. + */ + private JLabel titleLabel; + /** + * Base title text used when rendering the page header (date suffixes are appended). + */ + private final String baseTitle = "Abacus Skills Progression"; + + /** + * Construct the Abacus page for the given student and session date. + * + * @param studentName the selected student's display name (may be null before selection) + * @param date the date to associate with created progress sessions + * @param lineGraph the shared graph component used to visualize results + */ + public Abacus(final String studentName, final LocalDate date, final JLineGraph lineGraph) { + this.studentNameParam = (studentName == null || studentName.trim().isEmpty()) ? com.studentgui.apphelpers.Helpers.defaultStudent() : studentName; + this.dateParam = date; + this.lineGraph = lineGraph; // Use the passed in graph instance + setLayout(new BorderLayout()); + + // Initialize skills array and layout using canonical abacus parts + this.parts = new String[][]{ + {"P1_1","1.1 Setting Numbers"},{"P1_2","1.2 Clearing Beads"},{"P1_3","1.3 Place Value"},{"P1_4","1.4 Vocabulary"}, + {"P2_1","2.1 Addition of Single Digit Numbers"},{"P2_2","2.2 Direct Addition"},{"P2_3","2.3 Indirect Addition"}, + {"P3_1","3.1 Subtraction of Single Digit Numbers"},{"P3_2","3.2 Direct Subtraction"},{"P3_3","3.3 Indirect Subtraction"}, + {"P4_1","4.1 Multiplication – 2+ Digit Multiplicand 1-Digit Multiplier"},{"P4_2","4.2 Multiplication – 2+ Digit Multiplicand AND Multiplier"}, + {"P5_1","5.1 Division – 2+ Digit Dividend 1-Digit Divisor"},{"P5_2","5.2 Division – 2+ Digit Dividend AND 1 Digit Divisor"}, + {"P6_1","6.1 Addition of Decimals"},{"P6_2","6.2 Subtraction of Decimals"},{"P6_3","6.3 Multiplication of Decimals"},{"P6_4","6.4 Division of Decimals"}, + {"P7_1","7.1 Addition of Fractions"},{"P7_2","7.2 Subtraction of Fractions"},{"P7_3","7.3 Multiplication of Fractions"},{"P7_4","7.4 Division of Fractions"}, + {"P8_1","8.1 Percent"},{"P8_2","8.2 Square Root"} + }; + + // Panel for data entry + JPanel dataEntryPanel = new JPanel(); + dataEntryPanel.setLayout(new GridBagLayout()); + JPanel view = new JPanel(new BorderLayout()); + view.add(dataEntryPanel, BorderLayout.NORTH); + view.setBorder(javax.swing.BorderFactory.createEmptyBorder(20,20,20,20)); + JScrollPane dataEntryScrollPane = new JScrollPane(view); + dataEntryScrollPane.setVerticalScrollBarPolicy(JScrollPane.VERTICAL_SCROLLBAR_AS_NEEDED); + dataEntryScrollPane.setHorizontalScrollBarPolicy(JScrollPane.HORIZONTAL_SCROLLBAR_NEVER); + dataEntryScrollPane.getAccessibleContext().setAccessibleName("Abacus data entry scroll pane"); + + GridBagConstraints gbc = new GridBagConstraints(); + gbc.insets = new Insets(2, 2, 2, 2); + gbc.fill = GridBagConstraints.HORIZONTAL; + gbc.weightx = 1.0; + gbc.weighty = 0.0; + + this.titleLabel = new JLabel(baseTitle); + this.titleLabel.setFont(this.titleLabel.getFont().deriveFont(Font.BOLD, 16)); + gbc.gridx = 0; + gbc.gridy = 0; + gbc.gridwidth = GridBagConstraints.REMAINDER; + dataEntryPanel.add(titleLabel, gbc); + + gbc.gridy = 1; + gbc.gridwidth = GridBagConstraints.REMAINDER; + gbc.ipady = 20; + dataEntryPanel.add(new JPanel(), gbc); + + // visual spacing controlled by PhaseScoreField and layout + + String[] labels = java.util.Arrays.stream(this.parts).map(x->x[1]).toArray(String[]::new); + int maxPx = com.studentgui.uicomp.PhaseScoreField.computeMaxLabelPixelWidth(titleLabel.getFont(), labels); + com.studentgui.uicomp.PhaseScoreField.setGlobalLabelWidth(Math.min(320, Math.max(140, maxPx + 50))); + skillFields = new com.studentgui.uicomp.PhaseScoreField[this.parts.length]; + for (int i = 0; i < this.parts.length; i++) { + gbc.gridy = i + 2; + gbc.gridx = 0; + gbc.gridwidth = 1; + com.studentgui.uicomp.PhaseScoreField field = new com.studentgui.uicomp.PhaseScoreField(this.parts[i][1], 0); + field.setName("abacus_" + this.parts[i][0]); + field.getAccessibleContext().setAccessibleName(this.parts[i][1]); + field.setToolTipText("Enter a numeric score for " + this.parts[i][1]); + gbc.gridx = 0; gbc.gridwidth = 2; gbc.insets = new Insets(2, 2, 2, 2); + dataEntryPanel.add(field, gbc); + skillFields[i] = field; + gbc.gridx = 2; gbc.gridwidth = 1; gbc.insets = new Insets(2, 0, 2, 2); + dataEntryPanel.add(new JPanel(), gbc); + } + + gbc.gridy = this.parts.length + 3; + gbc.gridx = 0; + gbc.gridwidth = GridBagConstraints.REMAINDER; + gbc.weighty = 1.0; + dataEntryPanel.add(new JPanel(), gbc); + + // Place Submit and Open Latest side-by-side with IOS-like height + gbc.gridy = this.parts.length + 4; + gbc.weighty = 0.0; + gbc.gridx = 0; + gbc.gridwidth = 1; + JButton submitDataButton = new JButton("Submit Data"); + submitDataButton.setPreferredSize(new java.awt.Dimension(0, 32)); + submitDataButton.addActionListener((ActionEvent e) -> { submitData(); refreshGraph(); }); + submitDataButton.setMnemonic(KeyEvent.VK_S); + submitDataButton.setToolTipText("Save Abacus scores for the selected student (Alt+S)"); + submitDataButton.getAccessibleContext().setAccessibleName("Submit Abacus Data"); + dataEntryPanel.add(submitDataButton, gbc); + + gbc.gridx = 1; + JButton openLatestBtn = new JButton("Open Latest Plot"); + openLatestBtn.setPreferredSize(new java.awt.Dimension(0, 32)); + openLatestBtn.addActionListener((ActionEvent e) -> { + java.nio.file.Path p = com.studentgui.apphelpers.Helpers.latestPlotPath(this.studentNameParam, "Abacus"); + if (p == null) { + com.studentgui.apphelpers.UiNotifier.show("No Abacus plot found for student"); + } else { + try { + java.awt.Desktop.getDesktop().open(p.toFile()); + } catch (java.io.IOException | UnsupportedOperationException | SecurityException ex) { + com.studentgui.apphelpers.UiNotifier.show("Unable to open plot: " + p.getFileName().toString()); + } + } + }); + dataEntryPanel.add(openLatestBtn, gbc); + + gbc.gridx = 2; gbc.gridwidth = GridBagConstraints.REMAINDER; + dataEntryPanel.add(new JPanel(), gbc); + + add(dataEntryScrollPane, BorderLayout.CENTER); + + // Add existing graph reference + add(lineGraph, BorderLayout.SOUTH); + + SwingUtilities.invokeLater(() -> { + dataEntryPanel.setPreferredSize(dataEntryPanel.getPreferredSize()); + updateTitleDate(); + revalidate(); + }); + + // Ensure application folders and DB schema exist before DB operations + com.studentgui.apphelpers.Helpers.createFolderHierarchy(); + initDatabase(); + refreshGraph(); + } + + /** + * Ensure the canonical progress-type and assessment parts for Abacus exist + * in the normalized database schema. Safe to call multiple times. + */ + private void initDatabase() { + try { + int ptId = com.studentgui.apphelpers.Database.getOrCreateProgressType("Abacus"); + // Use the canonical part codes declared on this page so parts are created + // with the expected codes like "P1_1", "P1_2", ... + String[] codes = new String[this.parts.length]; + for (int i = 0; i < this.parts.length; i++) { + codes[i] = this.parts[i][0]; + } + com.studentgui.apphelpers.Database.ensureAssessmentParts(ptId, codes); + try { + com.studentgui.apphelpers.Database.cleanupAssessmentParts(ptId, codes); + } catch (SQLException se) { + LOG.warn("Could not cleanup legacy parts for Abacus", se); + } + } catch (SQLException e) { + LOG.error("SQL error initializing Abacus parts", e); + } + } + + /** + * Read input fields, validate numeric input, and persist the values as a + * new progress session for the selected student. + */ + private void submitData() { + if (studentNameParam == null || studentNameParam.trim().isEmpty()) { + JOptionPane.showMessageDialog(this, "Please select a student before submitting Abacus data.", "Missing student", JOptionPane.WARNING_MESSAGE); + return; + } + + try { + int studentId = com.studentgui.apphelpers.Database.getOrCreateStudent(studentNameParam); + int ptId = com.studentgui.apphelpers.Database.getOrCreateProgressType("Abacus"); + int sessionId = com.studentgui.apphelpers.Database.createProgressSession(studentId, ptId, dateParam); + + String[] codes = new String[this.parts.length]; + int[] scores = new int[this.parts.length]; + for (int i = 0; i < this.parts.length; i++) { + codes[i] = this.parts[i][0]; + scores[i] = skillFields[i].getValue(); + } + com.studentgui.apphelpers.Database.insertAssessmentResults(sessionId, ptId, codes, scores); + LOG.info("Data submitted successfully via normalized schema."); + com.studentgui.apphelpers.UiNotifier.show("Abacus data saved."); + // Also persist this session as a JSON file in the student's folder (timestamped per-session) + com.studentgui.apphelpers.dto.AssessmentPayload payload = new com.studentgui.apphelpers.dto.AssessmentPayload(sessionId, codes, scores); + java.nio.file.Path jsonOut = com.studentgui.apphelpers.SessionJsonWriter.writeSessionJson(this.studentNameParam, "Abacus", payload, sessionId); + if (jsonOut == null) { + LOG.warn("Unable to save Abacus session JSON for sessionId={}", sessionId); + } + // Generate per-phase PNGs (time-series) and a markdown report for this session + try { + java.nio.file.Path plotsOut = com.studentgui.apphelpers.Helpers.studentPlotsDir(this.studentNameParam); + java.nio.file.Path reportsOut = com.studentgui.apphelpers.Helpers.studentReportsDir(this.studentNameParam); + java.nio.file.Files.createDirectories(plotsOut); + java.nio.file.Files.createDirectories(reportsOut); + java.time.format.DateTimeFormatter df = java.time.format.DateTimeFormatter.ISO_DATE; + String dateStr = (this.dateParam != null ? this.dateParam.format(df) : java.time.LocalDate.now().toString()); + String baseName = "Abacus-" + sessionId + "-" + dateStr; + + // Fetch recent dated sessions (oldest first) to build time-series plots. + com.studentgui.apphelpers.Database.ResultsWithDates rwd = com.studentgui.apphelpers.Database.fetchLatestAssessmentResultsWithDates(this.studentNameParam, "Abacus", Integer.MAX_VALUE); + + java.util.Map groups = null; + if (rwd != null && rwd.rows != null && !rwd.rows.isEmpty()) { + // Build human-friendly labels from this.parts and render time-series grouped charts + String[] labels = new String[this.parts.length]; + for (int i = 0; i < this.parts.length; i++) { + labels[i] = this.parts[i][1]; + } + lineGraph.updateWithGroupedDataByDate(rwd.dates, rwd.rows, codes, labels); + // Persist each group as a PNG (time-series image) + groups = lineGraph.saveGroupedCharts(plotsOut, baseName, 1000, 240); + // Use the most-recent session date for the report header if available + java.time.LocalDate headerDate = rwd.dates.get(rwd.dates.size() - 1); + dateStr = headerDate.format(df); + } else { + // Fallback: render only the latest session snapshot + java.util.List> rows = new java.util.ArrayList<>(); + java.util.List latest = new java.util.ArrayList<>(); + for (int v : scores) { + latest.add(v); + } + rows.add(latest); + lineGraph.updateWithGroupedData(rows, codes); + groups = lineGraph.saveGroupedCharts(plotsOut, baseName, 1000, 240); + } + + // Generate markdown report + if (groups == null) { + groups = new java.util.LinkedHashMap<>(); + } + StringBuilder md = new StringBuilder(); + md.append("# ").append(this.studentNameParam == null ? "Unknown Student" : this.studentNameParam).append(" - ").append(dateStr).append("\n\n"); + for (java.util.Map.Entry e : groups.entrySet()) { + md.append("## ").append(e.getKey()).append("\n\n"); + md.append("![](./").append(e.getValue().getFileName().toString()).append(")\n\n"); + } + java.nio.file.Path mdFile = reportsOut.resolve(baseName + ".md"); + // images live in ../plots relative to reports + String mdText = md.toString().replace("![](./", "![](../plots/"); + java.nio.file.Files.writeString(mdFile, mdText, java.nio.charset.StandardCharsets.UTF_8); + LOG.info("Wrote Abacus session report {} with {} group images", mdFile, groups.size()); + // Also produce a simple HTML report that embeds the PNGs and + // shows a scrollable legend under each plot. + try { + String[] palette = new String[] {"#1b9e77","#d95f02","#7570b3","#e7298a","#66a61e","#e6ab02","#a6761d","#666666"}; + + // Build a map of group -> list of part indexes to recreate legend order + java.util.LinkedHashMap> groupsIdx = new java.util.LinkedHashMap<>(); + for (int i = 0; i < codes.length; i++) { + String code = codes[i]; + String grp = code != null && code.contains("_") ? code.split("_")[0] : code; + groupsIdx.computeIfAbsent(grp, k -> new java.util.ArrayList<>()).add(i); + } + + StringBuilder html = new StringBuilder(); + html.append("\n"); + html.append(this.studentNameParam == null ? "Student Report" : this.studentNameParam).append(" - ").append(dateStr); + html.append(""); + html.append(""); + html.append(""); + html.append("

").append(this.studentNameParam == null ? "Unknown Student" : this.studentNameParam).append(" - ").append(dateStr).append("

"); + + for (java.util.Map.Entry e2 : groups.entrySet()) { + String grp = e2.getKey(); + String imgName = e2.getValue().getFileName().toString(); + html.append("

").append(grp).append("

"); + html.append("
\"").append(grp).append("\"
"); + + // legend for this group + java.util.List idxs = groupsIdx.getOrDefault(grp, new java.util.ArrayList<>()); + html.append("
"); + for (int s = 0; s < idxs.size(); s++) { + int idx = idxs.get(s); + String code = codes[idx]; + String human = this.parts[idx][1]; + String seriesName = code + " - " + human; + String color = palette[s % palette.length]; + html.append("
"); + html.append("
").append(seriesName).append("
"); + } + html.append("
"); + } + + html.append(""); + java.nio.file.Path htmlFile = reportsOut.resolve(baseName + ".html"); + // adjust image src to point to ../plots + String htmlStr = html.toString().replace("src=\"./", "src=\"../plots/"); + java.nio.file.Files.writeString(htmlFile, htmlStr, java.nio.charset.StandardCharsets.UTF_8); + LOG.info("Wrote Abacus HTML session report {}", htmlFile); + } catch (java.io.IOException ioex) { + LOG.warn("Unable to write HTML report: {}", ioex.toString()); + } + } catch (java.io.IOException | SQLException ex) { + LOG.warn("Unable to save Abacus per-phase plots or markdown report: {}", ex.toString()); + } + } catch (SQLException e) { + LOG.error("SQL error in submitData", e); + JOptionPane.showMessageDialog(this, "Database error saving Abacus data: " + e.getMessage(), "Database error", JOptionPane.ERROR_MESSAGE); + } + } + + /** + * Load recent assessment sessions for the selected student and update the + * shared {@link JLineGraph} with the returned metric series. + */ + private void refreshGraph() { + try { + com.studentgui.apphelpers.Database.ResultsWithDates rwd = com.studentgui.apphelpers.Database.fetchLatestAssessmentResultsWithDates(studentNameParam, "Abacus", Integer.MAX_VALUE); + String[] codes = new String[this.parts.length]; + for (int i = 0; i < this.parts.length; i++) { + codes[i] = this.parts[i][0]; + } + if (rwd != null && rwd.rows != null && !rwd.rows.isEmpty()) { + // Use the date-aware grouped plotter so X axis is dates and each + // skill within a phase is a separate line series. + String[] labels = new String[this.parts.length]; + for (int i = 0; i < this.parts.length; i++) { + labels[i] = this.parts[i][1]; + } + lineGraph.updateWithGroupedDataByDate(rwd.dates, rwd.rows, codes, labels); + LOG.debug("Graph updated with {} dated sessions", rwd.rows.size()); + } else { + LOG.info("No data to plot; showing grouped placeholders."); + lineGraph.showEmptyGrouped(codes); + } + } catch (SQLException e) { + LOG.error("SQL error refreshing graph", e); + } + } + @Override + public void dateChanged(final LocalDate newDate) { + this.dateParam = newDate; + // When the global date changes, update the graph to reflect any + // date-related logic (most refreshGraph implementations load + // recent sessions independent of the selected session date, but + // updating here keeps the saved date in sync for future submits). + SwingUtilities.invokeLater(this::refreshGraph); + } + + @Override + public void studentChanged(final String newStudent) { + this.studentNameParam = newStudent; + SwingUtilities.invokeLater(() -> { + refreshGraph(); + updateTitleDate(); + }); + } + + private void updateTitleDate() { + try { + String dateStr = this.dateParam != null ? this.dateParam.toString() : java.time.LocalDate.now().toString(); + this.titleLabel.setText(baseTitle + " - " + dateStr); + } catch (Exception ex) { + this.titleLabel.setText(baseTitle); + } + } + + +} diff --git a/src/main/java/com/studentgui/apppages/Braille.java b/src/main/java/com/studentgui/apppages/Braille.java index 039dda1..5b14e22 100644 --- a/src/main/java/com/studentgui/apppages/Braille.java +++ b/src/main/java/com/studentgui/apppages/Braille.java @@ -1,416 +1,455 @@ -package com.studentgui.apppages; - -import java.awt.BorderLayout; -import java.awt.Font; -import java.awt.GridBagConstraints; -import java.awt.GridBagLayout; -import java.awt.Insets; -import java.awt.event.ActionEvent; -import java.awt.event.KeyEvent; -import java.sql.SQLException; -import java.time.LocalDate; -import java.util.List; - -import javax.swing.JButton; -import javax.swing.JLabel; -import javax.swing.JOptionPane; -import javax.swing.JPanel; -import javax.swing.JScrollPane; -import javax.swing.SwingUtilities; - -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; - -/** - * Braille skills progression UI page. - * - * Displays a list of braille-related skill input fields and provides controls - * to persist entries to the normalized database schema. The page updates a - * shared {@link JLineGraph} to visualize recent results for the selected - * student. - */ -public class Braille extends JPanel implements com.studentgui.app.DateChangeListener, com.studentgui.app.StudentChangeListener { - private static final Logger LOG = LoggerFactory.getLogger(Braille.class); - - /** Array of input components representing each Braille skill. */ - private final com.studentgui.uicomp.PhaseScoreField[] skillFields; - /** Parts list for Braille (code,label) */ - private final String[][] parts; - /** Flat list of part codes (derived from parts) */ - private final String[] partCodes; - /** Shared graph used to plot recent results. */ - private final JLineGraph lineGraph; // Reference to the JLineGraph instance - /** Selected student display name (may be null or placeholder). */ - private String studentNameParam; - /** Session date used when creating progress sessions. */ - private LocalDate dateParam; - /** Title label component displayed in the page header. */ - private JLabel titleLabel; - /** Base title text for the Braille page; a date suffix may be appended for display. */ - private final String baseTitle = "Braille Skills Progression"; - - /** - * Construct the Braille skills page for a given student and date. - * - * @param studentName the selected student name (may be null before selection) - * @param date the session date to use when creating a progress session - * @param lineGraph shared graph component used to display recent results - */ - public Braille(final String studentName, final LocalDate date, final JLineGraph lineGraph) { - this.lineGraph = lineGraph; // Use the passed in graph instance - this.studentNameParam = (studentName == null || studentName.trim().isEmpty()) ? com.studentgui.apphelpers.Helpers.defaultStudent() : studentName; - this.dateParam = date != null ? date : LocalDate.now(); - setLayout(new BorderLayout()); - - // Detailed Braille parts (code, visible label) - this.parts = new String[][]{ - {"P1_1","1.1. Track left to right"},{"P1_2","1.2. Track top to bottom"},{"P1_3","1.3. Discriminate shapes"},{"P1_4","1.4. Discriminate braille characters"}, - {"P2_1","2.1. Mangold Progression: G C L"},{"P2_2","2.2. Mangold Progression: D Y"},{"P2_3","2.3. Mangold Progression: A B"},{"P2_4","2.4. Mangold Progression: S"}, - {"P2_5","2.5. Mangold Progression: W"},{"P2_6","2.6. Mangold Progression: P O"},{"P2_7","2.7. Mangold Progression: K"},{"P2_8","2.8. Mangold Progression: R"}, - {"P2_9","2.9. Mangold Progression: M E"},{"P2_10","2.10. Mangold Progression: H"},{"P2_11","2.11. Mangold Progression: N X"},{"P2_12","2.12. Mangold Progression: Z F"}, - {"P2_13","2.13. Mangold Progression: U T"},{"P2_14","2.14. Mangold Progression: Q I"},{"P2_15","2.15. Mangold Progression: V J"}, - {"P3_1","3.1. Alphabetic Wordsigns"},{"P3_2","3.2. Braille Numbers"},{"P3_3","3.3. Punctuation"},{"P3_4","3.4. Strong Contractions (AND OF FOR WITH THE)"}, - {"P3_5","3.5. Strong Groupsigns (CH GH SH TH WH ED ER OU OW ST AR ING)"},{"P3_6","3.6. Strong Wordsigns (CH SH TH WH OU ST)"},{"P3_7","3.7. Lower Groupsigns (BE CON DIS)"}, - {"P3_8","3.8. Lower Groupsigns (EA BB CC FF GG)"},{"P3_9","3.9. Lower Groupsigns/Wordsigns (EN IN)"},{"P3_10","3.10. Lower Wordsigns (BE HIS WAS WERE)"}, - {"P3_11","3.11. Dot 5 Contractions"},{"P3_12","3.12. Dot 45 Contractions"},{"P3_13","3.13. Dot 456 Contractions"},{"P3_14","3.14. Final Letter Groupsigns"}, - {"P3_15","3.15. Shortform Words"},{"P4_1","4.1. Grade 1 Indicators"},{"P4_2","4.2. Capitals Indicators"},{"P4_3","4.3. Numeric Mode and Spatial math"}, - {"P4_4","4.4. Typeform Indicators (ITALIC SCRIPT UNDERLINE BOLDFACE)"},{"P5_1","5.1. Page Numbering"},{"P5_2","5.2. Headings"},{"P5_3","5.3. Lists"}, - {"P5_4","5.4. Poety / Drama"},{"P6_1","6.1. Operation and Comparison Signs"},{"P6_2","6.2. Grade 1 Mode"},{"P6_3","6.3. Special Print Symbols"}, - {"P6_4","6.4. Omission Marks"},{"P6_5","6.5. Shape Indicators"},{"P6_6","6.6. Roman Numerals"},{"P6_7","6.7. Fractions"}, - {"P7_1","7.1. Grade 1 Mode and Algebra"},{"P7_2","7.2. Grade 1 Mode and Fractions"},{"P7_3","7.3. Advanced Operation and Comparison Signs"},{"P7_4","7.4. Indices"}, - {"P7_5","7.5. Roots and Radicals"},{"P7_6","7.6. Miscellaneous Shape Indicators"},{"P7_7","7.7. Functions"},{"P7_8","7.8. Greek letters"}, - {"P8_1","8.1. Functions"},{"P8_2","8.2. Modifiers Bars and Dots"},{"P8_3","8.3. Modifiers Arrows and Limits"},{"P8_4","8.4. Probability"}, - {"P8_5","8.5. Calculus: Differentiation"},{"P8_6","8.6. Calculus: Integration"},{"P8_7","8.7. Vertical Bars"} - }; - this.partCodes = new String[this.parts.length]; - for (int i = 0; i < this.parts.length; i++) { - this.partCodes[i] = this.parts[i][0]; - } - - // Panel for data entry - JPanel dataEntryPanel = new JPanel(); - dataEntryPanel.setLayout(new GridBagLayout()); - JPanel view = new JPanel(new BorderLayout()); - view.add(dataEntryPanel, BorderLayout.NORTH); - view.setBorder(javax.swing.BorderFactory.createEmptyBorder(20,20,20,20)); - JScrollPane dataEntryScrollPane = new JScrollPane(view); - dataEntryScrollPane.setVerticalScrollBarPolicy(JScrollPane.VERTICAL_SCROLLBAR_AS_NEEDED); - dataEntryScrollPane.setHorizontalScrollBarPolicy(JScrollPane.HORIZONTAL_SCROLLBAR_NEVER); - dataEntryScrollPane.getAccessibleContext().setAccessibleName("Braille data entry scroll pane"); - - GridBagConstraints gbc = new GridBagConstraints(); - gbc.insets = new Insets(2, 2, 2, 2); - gbc.fill = GridBagConstraints.HORIZONTAL; - gbc.weightx = 1.0; - gbc.weighty = 0.0; - - this.titleLabel = new JLabel(baseTitle); - this.titleLabel.setFont(this.titleLabel.getFont().deriveFont(Font.BOLD, 16)); - gbc.gridx = 0; - gbc.gridy = 0; - gbc.gridwidth = GridBagConstraints.REMAINDER; - dataEntryPanel.add(this.titleLabel, gbc); - - gbc.gridy = 1; - gbc.gridwidth = GridBagConstraints.REMAINDER; - gbc.ipady = 20; - dataEntryPanel.add(new JPanel(), gbc); - - // compute longest label width to align inputs - String[] labels = java.util.Arrays.stream(parts).map(x->x[1]).toArray(String[]::new); - int maxPx = com.studentgui.uicomp.PhaseScoreField.computeMaxLabelPixelWidth(titleLabel.getFont(), labels); - com.studentgui.uicomp.PhaseScoreField.setGlobalLabelWidth(Math.min(320, Math.max(140, maxPx + 50))); - skillFields = new com.studentgui.uicomp.PhaseScoreField[this.parts.length]; - for (int i = 0; i < this.parts.length; i++) { - gbc.gridy = i + 2; - gbc.gridx = 0; - gbc.gridwidth = 1; - com.studentgui.uicomp.PhaseScoreField skillField = new com.studentgui.uicomp.PhaseScoreField(this.parts[i][1], 0); - skillField.setName("braille_" + this.parts[i][0]); - skillField.getAccessibleContext().setAccessibleName(this.parts[i][1]); - skillField.setToolTipText("Enter a numeric score for " + this.parts[i][1]); - gbc.gridx = 0; gbc.gridwidth = 2; gbc.insets = new Insets(2, 2, 2, 2); - dataEntryPanel.add(skillField, gbc); - skillFields[i] = skillField; - gbc.gridx = 2; gbc.insets = new Insets(2, 0, 2, 2); - dataEntryPanel.add(new JPanel(), gbc); - } - - gbc.gridy = this.parts.length + 3; - gbc.gridx = 0; - gbc.gridwidth = GridBagConstraints.REMAINDER; - gbc.weighty = 1.0; - dataEntryPanel.add(new JPanel(), gbc); - - // Place Submit and Open Latest side-by-side (match IOS/ScreenReader style) - gbc.gridy = this.parts.length + 4; - gbc.weighty = 0.0; - gbc.gridx = 0; - gbc.gridwidth = 1; - gbc.anchor = GridBagConstraints.WEST; - JButton submitDataButton = new JButton("Submit Data"); - submitDataButton.setPreferredSize(new java.awt.Dimension(0, 32)); - submitDataButton.addActionListener((ActionEvent e) -> { submitData(); refreshGraph(); }); - submitDataButton.setMnemonic(KeyEvent.VK_S); - submitDataButton.setToolTipText("Save Braille scores for the selected student (Alt+S)"); - submitDataButton.getAccessibleContext().setAccessibleName("Submit Braille Data"); - dataEntryPanel.add(submitDataButton, gbc); - - gbc.gridx = 1; - JButton openLatestBtn = new JButton("Open Latest Plot"); - openLatestBtn.setPreferredSize(new java.awt.Dimension(0, 32)); - openLatestBtn.addActionListener((ActionEvent e) -> { - java.nio.file.Path p = com.studentgui.apphelpers.Helpers.latestPlotPath(this.studentNameParam, "Braille"); - if (p == null) { - com.studentgui.apphelpers.UiNotifier.show("No Braille plot found for student"); - } else { - try { - java.awt.Desktop.getDesktop().open(p.toFile()); - } catch (java.io.IOException | UnsupportedOperationException | SecurityException ex) { - com.studentgui.apphelpers.UiNotifier.show("Unable to open plot: " + p.getFileName().toString()); - } - } - }); - dataEntryPanel.add(openLatestBtn, gbc); - - // consume remaining columns (if any) so layout stays compact - gbc.gridx = 2; gbc.gridwidth = GridBagConstraints.REMAINDER; gbc.anchor = GridBagConstraints.WEST; - dataEntryPanel.add(new JPanel(), gbc); - - add(dataEntryScrollPane, BorderLayout.CENTER); - - // Add existing graph reference - add(lineGraph, BorderLayout.SOUTH); - - SwingUtilities.invokeLater(() -> { - dataEntryPanel.setPreferredSize(dataEntryPanel.getPreferredSize()); - updateTitleDate(); - revalidate(); - }); - - com.studentgui.apphelpers.Helpers.createFolderHierarchy(); - initDatabase(); - refreshGraph(); - } - - /** - * Ensure the Braille progress-type and its assessment parts exist in the - * canonical schema. Safe to call repeatedly. - */ - private void initDatabase() { - // Ensure normalized schema parts for Braille exist - try { - int ptId = com.studentgui.apphelpers.Database.getOrCreateProgressType("Braille"); - // Use the canonical part codes defined in this.parts - com.studentgui.apphelpers.Database.ensureAssessmentParts(ptId, this.partCodes); - } catch (SQLException e) { - LOG.error("Error initializing Braille parts", e); - } - } - - /** - * Read entered skill values and persist them as a new progress session. - * Performs integer validation and informs the user on invalid input. - * - * Implementation note: arrays used to call {@code insertAssessmentResults} - * are allocated dynamically based on the actual number of parts - * ({@code partCodes.length}) so that the stored columns exactly match the - * plotted series. This fixes a previous issue where fixed-size arrays - * could become out-of-sync with the parts list. - */ - private void submitData() { - if (this.studentNameParam == null || this.studentNameParam.trim().isEmpty()) { - JOptionPane.showMessageDialog(this, "Please select a student before submitting Braille data.", "Missing student", JOptionPane.WARNING_MESSAGE); - return; - } - - try { - int studentId = com.studentgui.apphelpers.Database.getOrCreateStudent(this.studentNameParam); - int ptId = com.studentgui.apphelpers.Database.getOrCreateProgressType("Braille"); - int sessionId = com.studentgui.apphelpers.Database.createProgressSession(studentId, ptId, this.dateParam); - - // Allocate arrays based on the actual number of parts so that - // the submitted data and plotted series stay in sync. - String[] codes = new String[this.partCodes.length]; - int[] scores = new int[this.partCodes.length]; - for (int i = 0; i < this.partCodes.length; i++) { - codes[i] = this.partCodes[i]; - scores[i] = skillFields[i].getValue(); - } - com.studentgui.apphelpers.Database.insertAssessmentResults(sessionId, ptId, codes, scores); - LOG.info("Data submitted successfully via normalized schema."); - com.studentgui.apphelpers.UiNotifier.show("Braille data saved."); - com.studentgui.apphelpers.dto.AssessmentPayload payload = new com.studentgui.apphelpers.dto.AssessmentPayload(sessionId, codes, scores); - java.nio.file.Path jsonOut = com.studentgui.apphelpers.SessionJsonWriter.writeSessionJson(this.studentNameParam, "Braille", payload, sessionId); - if (jsonOut == null) { - LOG.warn("Unable to save Braille session JSON for sessionId={}", sessionId); - } - try { - java.nio.file.Path plotsOut = com.studentgui.apphelpers.Helpers.studentPlotsDir(this.studentNameParam); - java.nio.file.Path reportsOut = com.studentgui.apphelpers.Helpers.studentReportsDir(this.studentNameParam); - java.nio.file.Files.createDirectories(plotsOut); - java.nio.file.Files.createDirectories(reportsOut); - java.time.format.DateTimeFormatter df = java.time.format.DateTimeFormatter.ISO_DATE; - String dateStr = this.dateParam != null ? this.dateParam.format(df) : java.time.LocalDate.now().toString(); - String baseName = "Braille-" + sessionId + "-" + dateStr; - - com.studentgui.apphelpers.Database.ResultsWithDates rwd = com.studentgui.apphelpers.Database.fetchLatestAssessmentResultsWithDates(this.studentNameParam, "Braille", Integer.MAX_VALUE); - java.util.Map groups = null; - String[] labels = new String[this.parts.length]; - for (int i = 0; i < this.parts.length; i++) { - labels[i] = this.parts[i][1]; - } - if (rwd != null && rwd.rows != null && !rwd.rows.isEmpty()) { - lineGraph.updateWithGroupedDataByDate(rwd.dates, rwd.rows, this.partCodes, labels); - groups = lineGraph.saveGroupedCharts(plotsOut, baseName, 1000, 240); - java.time.LocalDate headerDate = rwd.dates.get(rwd.dates.size() - 1); - dateStr = headerDate.format(df); - } else { - java.util.List> rowsList = new java.util.ArrayList<>(); - java.util.List latest = new java.util.ArrayList<>(); - for (int v : scores) { - latest.add(v); - } - rowsList.add(latest); - lineGraph.updateWithGroupedData(rowsList, this.partCodes); - groups = lineGraph.saveGroupedCharts(plotsOut, baseName, 1000, 240); - } - - if (groups == null) { - groups = new java.util.LinkedHashMap<>(); - } - StringBuilder md = new StringBuilder(); - md.append("# ").append(this.studentNameParam == null ? "Unknown Student" : this.studentNameParam).append(" - ").append(dateStr).append("\n\n"); - for (java.util.Map.Entry e : groups.entrySet()) { - md.append("## ").append(e.getKey()).append("\n\n"); - md.append("![](./").append(e.getValue().getFileName().toString()).append(")\n\n"); - } - java.nio.file.Path mdFile = reportsOut.resolve(baseName + ".md"); - // images live in ../plots relative to reports - String mdText = md.toString().replace("![](./", "![](../plots/"); - java.nio.file.Files.writeString(mdFile, mdText, java.nio.charset.StandardCharsets.UTF_8); - - // HTML report using shared palette - try { - String[] palette = JLineGraph.PALETTE_HEX; - java.util.LinkedHashMap> groupsIdx = new java.util.LinkedHashMap<>(); - for (int i = 0; i < this.partCodes.length; i++) { - String code = this.partCodes[i]; - String grp = code != null && code.contains("_") ? code.split("_")[0] : code; - groupsIdx.computeIfAbsent(grp, k -> new java.util.ArrayList<>()).add(i); - } - StringBuilder html = new StringBuilder(); - html.append(""); - html.append(this.studentNameParam == null ? "Student Report" : this.studentNameParam).append(" - ").append(dateStr).append(""); - html.append(""); - html.append(""); - html.append("

").append(this.studentNameParam == null ? "Unknown Student" : this.studentNameParam).append(" - ").append(dateStr).append("

"); - for (java.util.Map.Entry e2 : groups.entrySet()) { - String grp = e2.getKey(); - String imgName = e2.getValue().getFileName().toString(); - html.append("

").append(grp).append("

"); - html.append("
\"").append(grp).append("\"
"); - java.util.List idxs = groupsIdx.getOrDefault(grp, new java.util.ArrayList<>()); - html.append("
"); - for (int s = 0; s < idxs.size(); s++) { - int idx = idxs.get(s); - String code = this.partCodes[idx]; - String human = this.parts[idx][1]; - String seriesName = code + " - " + human; - String color = palette[s % palette.length]; - html.append("
"); - html.append(""); - html.append("
"); - html.append(seriesName); - html.append("
"); - } - html.append("
"); - } - html.append(""); - java.nio.file.Path htmlFile = reportsOut.resolve(baseName + ".html"); - String htmlStr = html.toString().replace("src=\"./", "src=\"../plots/"); - java.nio.file.Files.writeString(htmlFile, htmlStr, java.nio.charset.StandardCharsets.UTF_8); - LOG.info("Wrote Braille HTML session report {}", htmlFile); - } catch (java.io.IOException ioex) { - LOG.warn("Unable to write Braille HTML report: {}", ioex.toString()); - } - - LOG.info("Wrote Braille session report {} with {} group images", mdFile, groups.size()); - } catch (java.io.IOException | SQLException ex) { - LOG.warn("Unable to save Braille per-phase plots or markdown report: {}", ex.toString()); - } - } catch (SQLException e) { - LOG.error("Unexpected error submitting braille data", e); - JOptionPane.showMessageDialog(this, "Database error saving Braille data: " + e.getMessage(), "Database error", JOptionPane.ERROR_MESSAGE); - } - } - /** - * Fetch recent assessment sessions and update the shared graph view. - */ - private void refreshGraph() { - try { - List> allSkillValues = com.studentgui.apphelpers.Database.fetchLatestAssessmentResults(this.studentNameParam, "Braille", 5); - // Note: pages should supply the selected student name; here the existing code used a passed-in studentName variable - // We will try to use the first skill field's content as a student name fallback; in the UI flow this should be provided. - // For now use a placeholder when no student is selected. - if (allSkillValues != null && !allSkillValues.isEmpty()) { - lineGraph.updateWithGroupedData(allSkillValues, this.partCodes); - // Write to the consolidated per-run data dumps file when enabled - if (Boolean.parseBoolean(com.studentgui.apphelpers.Settings.get("dump.enabled", "false"))) { - try { - String appHome = System.getProperty("APP_HOME", com.studentgui.apphelpers.Helpers.APP_HOME.toString()); - String ts = System.getProperty("LOG_TS", String.valueOf(java.time.Instant.now().getEpochSecond())); - java.nio.file.Path logDir = java.nio.file.Paths.get(appHome).resolve("logs"); - java.nio.file.Files.createDirectories(logDir); - java.nio.file.Path logFile = logDir.resolve("data_dumps_" + ts + ".log"); - StringBuilder sb = new StringBuilder(); - java.time.format.DateTimeFormatter dtf = java.time.format.DateTimeFormatter.ISO_DATE_TIME; - sb.append("[Braille]").append(System.lineSeparator()); - sb.append(java.time.LocalDateTime.now().format(dtf)).append(" - student=").append(this.studentNameParam).append(System.lineSeparator()); - sb.append("data=").append(allSkillValues.toString()).append(System.lineSeparator()); - sb.append(System.lineSeparator()); - java.nio.file.Files.writeString(logFile, sb.toString(), java.nio.charset.StandardCharsets.UTF_8, java.nio.file.StandardOpenOption.CREATE, java.nio.file.StandardOpenOption.APPEND); - } catch (java.io.IOException ioe) { - LOG.trace("Unable to write braille load log: {}", ioe.toString()); - } - } - } else { - LOG.info("No data to plot; showing grouped placeholders."); - lineGraph.showEmptyGrouped(this.partCodes); - } - } catch (SQLException e) { - LOG.error("SQL error refreshing braille graph", e); - } - } - - @Override - public void dateChanged(final LocalDate newDate) { - this.dateParam = newDate; - SwingUtilities.invokeLater(() -> { - refreshGraph(); - updateTitleDate(); - }); - } - - @Override - public void studentChanged(final String newStudent) { - this.studentNameParam = newStudent != null ? newStudent : "Unknown Student"; - SwingUtilities.invokeLater(() -> { - refreshGraph(); - updateTitleDate(); - }); - } - - private void updateTitleDate() { - try { - String dateStr = this.dateParam != null ? this.dateParam.toString() : java.time.LocalDate.now().toString(); - this.titleLabel.setText(baseTitle + " - " + dateStr); - } catch (Exception ex) { - this.titleLabel.setText(baseTitle); - } - } - - -} +package com.studentgui.apppages; + +import java.awt.BorderLayout; +import java.awt.Font; +import java.awt.GridBagConstraints; +import java.awt.GridBagLayout; +import java.awt.Insets; +import java.awt.event.ActionEvent; +import java.awt.event.KeyEvent; +import java.sql.SQLException; +import java.time.LocalDate; +import java.util.List; + +import javax.swing.JButton; +import javax.swing.JLabel; +import javax.swing.JOptionPane; +import javax.swing.JPanel; +import javax.swing.JScrollPane; +import javax.swing.SwingUtilities; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + * Braille skills progression assessment page. + * + *

Provides a comprehensive user interface for tracking student proficiency across + * 64 standardized Braille skills organized into 8 progressive phases following the + * Mangold Developmental Program sequence:

+ * + *
    + *
  • Phase 1 (P1_1–P1_4): Foundational tracking and discrimination skills
  • + *
  • Phase 2 (P2_1–P2_15): Mangold letter progression (G C L → V J)
  • + *
  • Phase 3 (P3_1–P3_15): Contractions, wordsigns, and Grade 2 Braille basics
  • + *
  • Phase 4 (P4_1–P4_4): Indicators (Grade 1, capitals, numeric mode, typeform)
  • + *
  • Phase 5 (P5_1–P5_4): Document formatting (page numbers, headings, lists, poetry)
  • + *
  • Phase 6 (P6_1–P6_7): Basic Nemeth Math Code (operations, shapes, fractions)
  • + *
  • Phase 7 (P7_1–P7_8): Advanced Math (algebra, indices, radicals, functions, Greek)
  • + *
  • Phase 8 (P8_1–P8_7): Higher mathematics (modifiers, calculus, probability)
  • + *
+ * + *

Data Flow and Persistence:

+ *
    + *
  • Each skill is represented by a {@link com.studentgui.uicomp.PhaseScoreField} accepting integer scores (0–4 typical range)
  • + *
  • On submission, values are persisted to the normalized schema via {@link com.studentgui.apphelpers.Database#insertAssessmentResults}
  • + *
  • A timestamped JSON export is written to {@code StudentDataFiles//Sessions/Braille/}
  • + *
  • Time-series plots are generated per phase group and saved as PNG images to {@code plots/}
  • + *
  • Markdown and HTML reports are generated combining all phase plots with legend and metadata
  • + *
+ * + *

Generated Artifacts:

+ *
    + *
  • JSON session file: {@code Braille--.json}
  • + *
  • Phase plots: {@code Braille---P.png} (8 phase groups)
  • + *
  • Markdown report: {@code reports/Braille--.md}
  • + *
  • HTML report: {@code reports/Braille--.html} with embedded plots and color-coded legends
  • + *
+ * + *

The shared {@link JLineGraph} component visualizes recent session trends for the selected + * student, grouped by phase to prevent overcrowding. This page implements {@link com.studentgui.app.DateChangeListener} + * and {@link com.studentgui.app.StudentChangeListener} to refresh data when the global student or date selection changes.

+ * + * @see com.studentgui.apphelpers.Database + * @see JLineGraph + * @see com.studentgui.uicomp.PhaseScoreField + */ +public class Braille extends JPanel implements com.studentgui.app.DateChangeListener, com.studentgui.app.StudentChangeListener { + private static final Logger LOG = LoggerFactory.getLogger(Braille.class); + + /** Array of input components representing each Braille skill. */ + private final com.studentgui.uicomp.PhaseScoreField[] skillFields; + /** Parts list for Braille (code,label) */ + private final String[][] parts; + /** Flat list of part codes (derived from parts) */ + private final String[] partCodes; + /** Shared graph used to plot recent results. */ + private final JLineGraph lineGraph; // Reference to the JLineGraph instance + /** Selected student display name (may be null or placeholder). */ + private String studentNameParam; + /** Session date used when creating progress sessions. */ + private LocalDate dateParam; + /** Title label component displayed in the page header. */ + private JLabel titleLabel; + /** Base title text for the Braille page; a date suffix may be appended for display. */ + private final String baseTitle = "Braille Skills Progression"; + + /** + * Construct the Braille skills page for a given student and date. + * + * @param studentName the selected student name (may be null before selection) + * @param date the session date to use when creating a progress session + * @param lineGraph shared graph component used to display recent results + */ + public Braille(final String studentName, final LocalDate date, final JLineGraph lineGraph) { + this.lineGraph = lineGraph; // Use the passed in graph instance + this.studentNameParam = (studentName == null || studentName.trim().isEmpty()) ? com.studentgui.apphelpers.Helpers.defaultStudent() : studentName; + this.dateParam = date != null ? date : LocalDate.now(); + setLayout(new BorderLayout()); + + // Detailed Braille parts (code, visible label) + this.parts = new String[][]{ + {"P1_1","1.1. Track left to right"},{"P1_2","1.2. Track top to bottom"},{"P1_3","1.3. Discriminate shapes"},{"P1_4","1.4. Discriminate braille characters"}, + {"P2_1","2.1. Mangold Progression: G C L"},{"P2_2","2.2. Mangold Progression: D Y"},{"P2_3","2.3. Mangold Progression: A B"},{"P2_4","2.4. Mangold Progression: S"}, + {"P2_5","2.5. Mangold Progression: W"},{"P2_6","2.6. Mangold Progression: P O"},{"P2_7","2.7. Mangold Progression: K"},{"P2_8","2.8. Mangold Progression: R"}, + {"P2_9","2.9. Mangold Progression: M E"},{"P2_10","2.10. Mangold Progression: H"},{"P2_11","2.11. Mangold Progression: N X"},{"P2_12","2.12. Mangold Progression: Z F"}, + {"P2_13","2.13. Mangold Progression: U T"},{"P2_14","2.14. Mangold Progression: Q I"},{"P2_15","2.15. Mangold Progression: V J"}, + {"P3_1","3.1. Alphabetic Wordsigns"},{"P3_2","3.2. Braille Numbers"},{"P3_3","3.3. Punctuation"},{"P3_4","3.4. Strong Contractions (AND OF FOR WITH THE)"}, + {"P3_5","3.5. Strong Groupsigns (CH GH SH TH WH ED ER OU OW ST AR ING)"},{"P3_6","3.6. Strong Wordsigns (CH SH TH WH OU ST)"},{"P3_7","3.7. Lower Groupsigns (BE CON DIS)"}, + {"P3_8","3.8. Lower Groupsigns (EA BB CC FF GG)"},{"P3_9","3.9. Lower Groupsigns/Wordsigns (EN IN)"},{"P3_10","3.10. Lower Wordsigns (BE HIS WAS WERE)"}, + {"P3_11","3.11. Dot 5 Contractions"},{"P3_12","3.12. Dot 45 Contractions"},{"P3_13","3.13. Dot 456 Contractions"},{"P3_14","3.14. Final Letter Groupsigns"}, + {"P3_15","3.15. Shortform Words"},{"P4_1","4.1. Grade 1 Indicators"},{"P4_2","4.2. Capitals Indicators"},{"P4_3","4.3. Numeric Mode and Spatial math"}, + {"P4_4","4.4. Typeform Indicators (ITALIC SCRIPT UNDERLINE BOLDFACE)"},{"P5_1","5.1. Page Numbering"},{"P5_2","5.2. Headings"},{"P5_3","5.3. Lists"}, + {"P5_4","5.4. Poety / Drama"},{"P6_1","6.1. Operation and Comparison Signs"},{"P6_2","6.2. Grade 1 Mode"},{"P6_3","6.3. Special Print Symbols"}, + {"P6_4","6.4. Omission Marks"},{"P6_5","6.5. Shape Indicators"},{"P6_6","6.6. Roman Numerals"},{"P6_7","6.7. Fractions"}, + {"P7_1","7.1. Grade 1 Mode and Algebra"},{"P7_2","7.2. Grade 1 Mode and Fractions"},{"P7_3","7.3. Advanced Operation and Comparison Signs"},{"P7_4","7.4. Indices"}, + {"P7_5","7.5. Roots and Radicals"},{"P7_6","7.6. Miscellaneous Shape Indicators"},{"P7_7","7.7. Functions"},{"P7_8","7.8. Greek letters"}, + {"P8_1","8.1. Functions"},{"P8_2","8.2. Modifiers Bars and Dots"},{"P8_3","8.3. Modifiers Arrows and Limits"},{"P8_4","8.4. Probability"}, + {"P8_5","8.5. Calculus: Differentiation"},{"P8_6","8.6. Calculus: Integration"},{"P8_7","8.7. Vertical Bars"} + }; + this.partCodes = new String[this.parts.length]; + for (int i = 0; i < this.parts.length; i++) { + this.partCodes[i] = this.parts[i][0]; + } + + // Panel for data entry + JPanel dataEntryPanel = new JPanel(); + dataEntryPanel.setLayout(new GridBagLayout()); + JPanel view = new JPanel(new BorderLayout()); + view.add(dataEntryPanel, BorderLayout.NORTH); + view.setBorder(javax.swing.BorderFactory.createEmptyBorder(20,20,20,20)); + JScrollPane dataEntryScrollPane = new JScrollPane(view); + dataEntryScrollPane.setVerticalScrollBarPolicy(JScrollPane.VERTICAL_SCROLLBAR_AS_NEEDED); + dataEntryScrollPane.setHorizontalScrollBarPolicy(JScrollPane.HORIZONTAL_SCROLLBAR_NEVER); + dataEntryScrollPane.getAccessibleContext().setAccessibleName("Braille data entry scroll pane"); + + GridBagConstraints gbc = new GridBagConstraints(); + gbc.insets = new Insets(2, 2, 2, 2); + gbc.fill = GridBagConstraints.HORIZONTAL; + gbc.weightx = 1.0; + gbc.weighty = 0.0; + + this.titleLabel = new JLabel(baseTitle); + this.titleLabel.setFont(this.titleLabel.getFont().deriveFont(Font.BOLD, 16)); + gbc.gridx = 0; + gbc.gridy = 0; + gbc.gridwidth = GridBagConstraints.REMAINDER; + dataEntryPanel.add(this.titleLabel, gbc); + + gbc.gridy = 1; + gbc.gridwidth = GridBagConstraints.REMAINDER; + gbc.ipady = 20; + dataEntryPanel.add(new JPanel(), gbc); + + // compute longest label width to align inputs + String[] labels = java.util.Arrays.stream(parts).map(x->x[1]).toArray(String[]::new); + int maxPx = com.studentgui.uicomp.PhaseScoreField.computeMaxLabelPixelWidth(titleLabel.getFont(), labels); + com.studentgui.uicomp.PhaseScoreField.setGlobalLabelWidth(Math.min(320, Math.max(140, maxPx + 50))); + skillFields = new com.studentgui.uicomp.PhaseScoreField[this.parts.length]; + for (int i = 0; i < this.parts.length; i++) { + gbc.gridy = i + 2; + gbc.gridx = 0; + gbc.gridwidth = 1; + com.studentgui.uicomp.PhaseScoreField skillField = new com.studentgui.uicomp.PhaseScoreField(this.parts[i][1], 0); + skillField.setName("braille_" + this.parts[i][0]); + skillField.getAccessibleContext().setAccessibleName(this.parts[i][1]); + skillField.setToolTipText("Enter a numeric score for " + this.parts[i][1]); + gbc.gridx = 0; gbc.gridwidth = 2; gbc.insets = new Insets(2, 2, 2, 2); + dataEntryPanel.add(skillField, gbc); + skillFields[i] = skillField; + gbc.gridx = 2; gbc.insets = new Insets(2, 0, 2, 2); + dataEntryPanel.add(new JPanel(), gbc); + } + + gbc.gridy = this.parts.length + 3; + gbc.gridx = 0; + gbc.gridwidth = GridBagConstraints.REMAINDER; + gbc.weighty = 1.0; + dataEntryPanel.add(new JPanel(), gbc); + + // Place Submit and Open Latest side-by-side (match IOS/ScreenReader style) + gbc.gridy = this.parts.length + 4; + gbc.weighty = 0.0; + gbc.gridx = 0; + gbc.gridwidth = 1; + gbc.anchor = GridBagConstraints.WEST; + JButton submitDataButton = new JButton("Submit Data"); + submitDataButton.setPreferredSize(new java.awt.Dimension(0, 32)); + submitDataButton.addActionListener((ActionEvent e) -> { submitData(); refreshGraph(); }); + submitDataButton.setMnemonic(KeyEvent.VK_S); + submitDataButton.setToolTipText("Save Braille scores for the selected student (Alt+S)"); + submitDataButton.getAccessibleContext().setAccessibleName("Submit Braille Data"); + dataEntryPanel.add(submitDataButton, gbc); + + gbc.gridx = 1; + JButton openLatestBtn = new JButton("Open Latest Plot"); + openLatestBtn.setPreferredSize(new java.awt.Dimension(0, 32)); + openLatestBtn.addActionListener((ActionEvent e) -> { + java.nio.file.Path p = com.studentgui.apphelpers.Helpers.latestPlotPath(this.studentNameParam, "Braille"); + if (p == null) { + com.studentgui.apphelpers.UiNotifier.show("No Braille plot found for student"); + } else { + try { + java.awt.Desktop.getDesktop().open(p.toFile()); + } catch (java.io.IOException | UnsupportedOperationException | SecurityException ex) { + com.studentgui.apphelpers.UiNotifier.show("Unable to open plot: " + p.getFileName().toString()); + } + } + }); + dataEntryPanel.add(openLatestBtn, gbc); + + // consume remaining columns (if any) so layout stays compact + gbc.gridx = 2; gbc.gridwidth = GridBagConstraints.REMAINDER; gbc.anchor = GridBagConstraints.WEST; + dataEntryPanel.add(new JPanel(), gbc); + + add(dataEntryScrollPane, BorderLayout.CENTER); + + // Add existing graph reference + add(lineGraph, BorderLayout.SOUTH); + + SwingUtilities.invokeLater(() -> { + dataEntryPanel.setPreferredSize(dataEntryPanel.getPreferredSize()); + updateTitleDate(); + revalidate(); + }); + + com.studentgui.apphelpers.Helpers.createFolderHierarchy(); + initDatabase(); + refreshGraph(); + } + + /** + * Ensure the Braille progress-type and its assessment parts exist in the + * canonical schema. Safe to call repeatedly. + */ + private void initDatabase() { + // Ensure normalized schema parts for Braille exist + try { + int ptId = com.studentgui.apphelpers.Database.getOrCreateProgressType("Braille"); + // Use the canonical part codes defined in this.parts + com.studentgui.apphelpers.Database.ensureAssessmentParts(ptId, this.partCodes); + } catch (SQLException e) { + LOG.error("Error initializing Braille parts", e); + } + } + + /** + * Read entered skill values and persist them as a new progress session. + * Performs integer validation and informs the user on invalid input. + * + * Implementation note: arrays used to call {@code insertAssessmentResults} + * are allocated dynamically based on the actual number of parts + * ({@code partCodes.length}) so that the stored columns exactly match the + * plotted series. This fixes a previous issue where fixed-size arrays + * could become out-of-sync with the parts list. + */ + private void submitData() { + if (this.studentNameParam == null || this.studentNameParam.trim().isEmpty()) { + JOptionPane.showMessageDialog(this, "Please select a student before submitting Braille data.", "Missing student", JOptionPane.WARNING_MESSAGE); + return; + } + + try { + int studentId = com.studentgui.apphelpers.Database.getOrCreateStudent(this.studentNameParam); + int ptId = com.studentgui.apphelpers.Database.getOrCreateProgressType("Braille"); + int sessionId = com.studentgui.apphelpers.Database.createProgressSession(studentId, ptId, this.dateParam); + + // Allocate arrays based on the actual number of parts so that + // the submitted data and plotted series stay in sync. + String[] codes = new String[this.partCodes.length]; + int[] scores = new int[this.partCodes.length]; + for (int i = 0; i < this.partCodes.length; i++) { + codes[i] = this.partCodes[i]; + scores[i] = skillFields[i].getValue(); + } + com.studentgui.apphelpers.Database.insertAssessmentResults(sessionId, ptId, codes, scores); + LOG.info("Data submitted successfully via normalized schema."); + com.studentgui.apphelpers.UiNotifier.show("Braille data saved."); + com.studentgui.apphelpers.dto.AssessmentPayload payload = new com.studentgui.apphelpers.dto.AssessmentPayload(sessionId, codes, scores); + java.nio.file.Path jsonOut = com.studentgui.apphelpers.SessionJsonWriter.writeSessionJson(this.studentNameParam, "Braille", payload, sessionId); + if (jsonOut == null) { + LOG.warn("Unable to save Braille session JSON for sessionId={}", sessionId); + } + try { + java.nio.file.Path plotsOut = com.studentgui.apphelpers.Helpers.studentPlotsDir(this.studentNameParam); + java.nio.file.Path reportsOut = com.studentgui.apphelpers.Helpers.studentReportsDir(this.studentNameParam); + java.nio.file.Files.createDirectories(plotsOut); + java.nio.file.Files.createDirectories(reportsOut); + java.time.format.DateTimeFormatter df = java.time.format.DateTimeFormatter.ISO_DATE; + String dateStr = this.dateParam != null ? this.dateParam.format(df) : java.time.LocalDate.now().toString(); + String baseName = "Braille-" + sessionId + "-" + dateStr; + + com.studentgui.apphelpers.Database.ResultsWithDates rwd = com.studentgui.apphelpers.Database.fetchLatestAssessmentResultsWithDates(this.studentNameParam, "Braille", Integer.MAX_VALUE); + java.util.Map groups = null; + String[] labels = new String[this.parts.length]; + for (int i = 0; i < this.parts.length; i++) { + labels[i] = this.parts[i][1]; + } + if (rwd != null && rwd.rows != null && !rwd.rows.isEmpty()) { + lineGraph.updateWithGroupedDataByDate(rwd.dates, rwd.rows, this.partCodes, labels); + groups = lineGraph.saveGroupedCharts(plotsOut, baseName, 1000, 240); + java.time.LocalDate headerDate = rwd.dates.get(rwd.dates.size() - 1); + dateStr = headerDate.format(df); + } else { + java.util.List> rowsList = new java.util.ArrayList<>(); + java.util.List latest = new java.util.ArrayList<>(); + for (int v : scores) { + latest.add(v); + } + rowsList.add(latest); + lineGraph.updateWithGroupedData(rowsList, this.partCodes); + groups = lineGraph.saveGroupedCharts(plotsOut, baseName, 1000, 240); + } + + if (groups == null) { + groups = new java.util.LinkedHashMap<>(); + } + StringBuilder md = new StringBuilder(); + md.append("# ").append(this.studentNameParam == null ? "Unknown Student" : this.studentNameParam).append(" - ").append(dateStr).append("\n\n"); + for (java.util.Map.Entry e : groups.entrySet()) { + md.append("## ").append(e.getKey()).append("\n\n"); + md.append("![](./").append(e.getValue().getFileName().toString()).append(")\n\n"); + } + java.nio.file.Path mdFile = reportsOut.resolve(baseName + ".md"); + // images live in ../plots relative to reports + String mdText = md.toString().replace("![](./", "![](../plots/"); + java.nio.file.Files.writeString(mdFile, mdText, java.nio.charset.StandardCharsets.UTF_8); + + // HTML report using shared palette + try { + String[] palette = JLineGraph.PALETTE_HEX; + java.util.LinkedHashMap> groupsIdx = new java.util.LinkedHashMap<>(); + for (int i = 0; i < this.partCodes.length; i++) { + String code = this.partCodes[i]; + String grp = code != null && code.contains("_") ? code.split("_")[0] : code; + groupsIdx.computeIfAbsent(grp, k -> new java.util.ArrayList<>()).add(i); + } + StringBuilder html = new StringBuilder(); + html.append(""); + html.append(this.studentNameParam == null ? "Student Report" : this.studentNameParam).append(" - ").append(dateStr).append(""); + html.append(""); + html.append(""); + html.append("

").append(this.studentNameParam == null ? "Unknown Student" : this.studentNameParam).append(" - ").append(dateStr).append("

"); + for (java.util.Map.Entry e2 : groups.entrySet()) { + String grp = e2.getKey(); + String imgName = e2.getValue().getFileName().toString(); + html.append("

").append(grp).append("

"); + html.append("
\"").append(grp).append("\"
"); + java.util.List idxs = groupsIdx.getOrDefault(grp, new java.util.ArrayList<>()); + html.append("
"); + for (int s = 0; s < idxs.size(); s++) { + int idx = idxs.get(s); + String code = this.partCodes[idx]; + String human = this.parts[idx][1]; + String seriesName = code + " - " + human; + String color = palette[s % palette.length]; + html.append("
"); + html.append(""); + html.append("
"); + html.append(seriesName); + html.append("
"); + } + html.append("
"); + } + html.append(""); + java.nio.file.Path htmlFile = reportsOut.resolve(baseName + ".html"); + String htmlStr = html.toString().replace("src=\"./", "src=\"../plots/"); + java.nio.file.Files.writeString(htmlFile, htmlStr, java.nio.charset.StandardCharsets.UTF_8); + LOG.info("Wrote Braille HTML session report {}", htmlFile); + } catch (java.io.IOException ioex) { + LOG.warn("Unable to write Braille HTML report: {}", ioex.toString()); + } + + LOG.info("Wrote Braille session report {} with {} group images", mdFile, groups.size()); + } catch (java.io.IOException | SQLException ex) { + LOG.warn("Unable to save Braille per-phase plots or markdown report: {}", ex.toString()); + } + } catch (SQLException e) { + LOG.error("Unexpected error submitting braille data", e); + JOptionPane.showMessageDialog(this, "Database error saving Braille data: " + e.getMessage(), "Database error", JOptionPane.ERROR_MESSAGE); + } + } + /** + * Fetch recent assessment sessions and update the shared graph view. + */ + private void refreshGraph() { + try { + List> allSkillValues = com.studentgui.apphelpers.Database.fetchLatestAssessmentResults(this.studentNameParam, "Braille", 5); + // Note: pages should supply the selected student name; here the existing code used a passed-in studentName variable + // We will try to use the first skill field's content as a student name fallback; in the UI flow this should be provided. + // For now use a placeholder when no student is selected. + if (allSkillValues != null && !allSkillValues.isEmpty()) { + lineGraph.updateWithGroupedData(allSkillValues, this.partCodes); + // Write to the consolidated per-run data dumps file when enabled + if (Boolean.parseBoolean(com.studentgui.apphelpers.Settings.get("dump.enabled", "false"))) { + try { + String appHome = System.getProperty("APP_HOME", com.studentgui.apphelpers.Helpers.APP_HOME.toString()); + String ts = System.getProperty("LOG_TS", String.valueOf(java.time.Instant.now().getEpochSecond())); + java.nio.file.Path logDir = java.nio.file.Paths.get(appHome).resolve("logs"); + java.nio.file.Files.createDirectories(logDir); + java.nio.file.Path logFile = logDir.resolve("data_dumps_" + ts + ".log"); + StringBuilder sb = new StringBuilder(); + java.time.format.DateTimeFormatter dtf = java.time.format.DateTimeFormatter.ISO_DATE_TIME; + sb.append("[Braille]").append(System.lineSeparator()); + sb.append(java.time.LocalDateTime.now().format(dtf)).append(" - student=").append(this.studentNameParam).append(System.lineSeparator()); + sb.append("data=").append(allSkillValues.toString()).append(System.lineSeparator()); + sb.append(System.lineSeparator()); + java.nio.file.Files.writeString(logFile, sb.toString(), java.nio.charset.StandardCharsets.UTF_8, java.nio.file.StandardOpenOption.CREATE, java.nio.file.StandardOpenOption.APPEND); + } catch (java.io.IOException ioe) { + LOG.trace("Unable to write braille load log: {}", ioe.toString()); + } + } + } else { + LOG.info("No data to plot; showing grouped placeholders."); + lineGraph.showEmptyGrouped(this.partCodes); + } + } catch (SQLException e) { + LOG.error("SQL error refreshing braille graph", e); + } + } + + @Override + public void dateChanged(final LocalDate newDate) { + this.dateParam = newDate; + SwingUtilities.invokeLater(() -> { + refreshGraph(); + updateTitleDate(); + }); + } + + @Override + public void studentChanged(final String newStudent) { + this.studentNameParam = newStudent != null ? newStudent : "Unknown Student"; + SwingUtilities.invokeLater(() -> { + refreshGraph(); + updateTitleDate(); + }); + } + + /** + * Update the page title label to include the current session date. + * Falls back to base title if date formatting fails. + */ + private void updateTitleDate() { + try { + String dateStr = this.dateParam != null ? this.dateParam.toString() : java.time.LocalDate.now().toString(); + this.titleLabel.setText(baseTitle + " - " + dateStr); + } catch (Exception ex) { + this.titleLabel.setText(baseTitle); + } + } + + +} diff --git a/src/main/java/com/studentgui/apppages/BrailleNote.java b/src/main/java/com/studentgui/apppages/BrailleNote.java index c1159d2..734914f 100644 --- a/src/main/java/com/studentgui/apppages/BrailleNote.java +++ b/src/main/java/com/studentgui/apppages/BrailleNote.java @@ -1,398 +1,458 @@ -package com.studentgui.apppages; -import java.awt.BorderLayout; -import java.awt.Font; -import java.awt.GridBagConstraints; -import java.awt.GridBagLayout; -import java.awt.Insets; -import java.awt.event.ActionEvent; -import java.awt.event.KeyEvent; -import java.sql.SQLException; -import java.time.LocalDate; -import java.util.List; - -import javax.swing.JButton; -import javax.swing.JLabel; -import javax.swing.JOptionPane; -import javax.swing.JPanel; -import javax.swing.JScrollPane; -import javax.swing.SwingUtilities; - -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; - -/** - * Braille note-taking skills progression page. - *

- * Presents a scrollable list of skill fields for a student and allows - * submission of scores into the canonical (normalized) SQLite schema. - * The page also displays a shared {@link JLineGraph} instance to visualize - * recent results. - *

- */ -public class BrailleNote extends JPanel implements com.studentgui.app.DateChangeListener, com.studentgui.app.StudentChangeListener { - private static final Logger LOG = LoggerFactory.getLogger(BrailleNote.class); - - /** Inputs for each BrailleNote skill. */ - private final com.studentgui.uicomp.PhaseScoreField[] skillFields; - /** Canonical assessment part codes and labels for BrailleNote. */ - private final String[][] parts; - /** Shared graph component for plotting results. */ - private final JLineGraph lineGraph; // Reference to the JLineGraph instance - /** Display name of the selected student (may be null). */ - private String studentNameParam; - /** Header title label for this page. */ - private JLabel titleLabel; - /** Base page title string used when rendering the header (date appended). */ - private final String baseTitle = "BrailleNote Skills Progression"; - /** Session date associated with persisted progress. */ - private LocalDate dateParam; - - /** - * Create the BrailleNote page for a specific student and date. - * - * @param studentName the selected student name (may be null until a student is chosen) - * @param date the date for the session (used when creating a progress session) - * @param lineGraph shared graph component used to display recent results - */ - public BrailleNote(String studentName, LocalDate date, JLineGraph lineGraph) { - this.studentNameParam = (studentName == null || studentName.trim().isEmpty()) ? com.studentgui.apphelpers.Helpers.defaultStudent() : studentName; - this.dateParam = date; - this.lineGraph = lineGraph; // Use the passed in graph instance - setLayout(new BorderLayout()); - - this.parts = new String[][]{ - {"P1_1","1.1 Physical Layout"},{"P1_2","1.2 Setup/Universal Commands"},{"P1_3","1.3 BNT+ Navigation"},{"P1_4","1.4 File Management"},{"P1_5","1.5 Word Processor"},{"P1_6","1.6 Email"},{"P1_7","1.7 Internet"},{"P1_8","1.8 Calculator"},{"P1_9","1.9 KeyMath"}, - {"P2_1","2.1 Calendar"},{"P2_2","2.2 KeyBRF"},{"P2_3","2.3 KeyFiles"},{"P2_4","2.4 KeyMail"},{"P2_5","2.5 KeyWeb"},{"P2_6","2.6 KeyCalc"},{"P2_7","2.7 KeyWord"}, - {"P3_1","3.1 KeySlides"},{"P3_2","3.2 KeyCode"},{"P3_3","3.3 Third Party Apps"},{"P3_4","3.4 Braille Input"},{"P3_5","3.5 Braille Output"},{"P3_6","3.6 Settings"},{"P3_7","3.7 Accessibility"}, - {"P4_1","4.1 Advanced File Management"},{"P4_2","4.2 Cloud Integration"},{"P4_3","4.3 Device Maintenance"}, - {"P5_1","5.1 Collaboration"},{"P5_2","5.2 Export/Import"},{"P5_3","5.3 Printing"},{"P5_4","5.4 Backup"}, - {"P6_1","6.1 App Installation"},{"P6_2","6.2 App Updates"},{"P6_3","6.3 Troubleshooting"}, - {"P7_1","7.1 Custom Shortcuts"},{"P7_2","7.2 Macros"},{"P7_3","7.3 Scripting"},{"P7_4","7.4 Automation"}, - {"P8_1","8.1 Bluetooth Devices"},{"P8_2","8.2 USB Devices"},{"P8_3","8.3 External Displays"},{"P8_4","8.4 Audio Output"},{"P8_5","8.5 Video Output"}, - {"P9_1","9.1 Security"},{"P9_2","9.2 User Accounts"},{"P9_3","9.3 Parental Controls"},{"P9_4","9.4 Network Settings"}, - {"P10_1","10.1 Speech Settings"},{"P10_2","10.2 Voice Profiles"},{"P10_3","10.3 Language Support"}, - {"P11_1","11.1 Firmware Updates"},{"P11_2","11.2 Diagnostics"},{"P11_3","11.3 Logs"},{"P11_4","11.4 Support"},{"P11_5","11.5 Warranty"}, - {"P12_1","12.1 Community Resources"},{"P12_2","12.2 Online Help"},{"P12_3","12.3 User Forums"},{"P12_4","12.4 Feedback"} - }; - - // Panel for data entry - JPanel dataEntryPanel = new JPanel(); - dataEntryPanel.setLayout(new GridBagLayout()); - JScrollPane dataEntryScrollPane = new JScrollPane(dataEntryPanel); - dataEntryScrollPane.setVerticalScrollBarPolicy(JScrollPane.VERTICAL_SCROLLBAR_AS_NEEDED); - dataEntryScrollPane.setHorizontalScrollBarPolicy(JScrollPane.HORIZONTAL_SCROLLBAR_NEVER); - dataEntryScrollPane.getAccessibleContext().setAccessibleName("BrailleNote data entry scroll pane"); - - GridBagConstraints gbc = new GridBagConstraints(); - gbc.insets = new Insets(5, 5, 5, 5); - gbc.fill = GridBagConstraints.HORIZONTAL; - gbc.weightx = 1.0; - gbc.weighty = 0.0; - - this.titleLabel = new JLabel(baseTitle); - this.titleLabel.setFont(this.titleLabel.getFont().deriveFont(Font.BOLD, 16)); - gbc.gridx = 0; - gbc.gridy = 0; - gbc.gridwidth = GridBagConstraints.REMAINDER; - dataEntryPanel.add(this.titleLabel, gbc); - - gbc.gridy = 1; - gbc.gridwidth = GridBagConstraints.REMAINDER; - gbc.ipady = 20; - dataEntryPanel.add(new JPanel(), gbc); - - // layout spacing handled by PhaseScoreField - - // compute pixel width using font metrics so labels align precisely - String[] labelsArr = java.util.Arrays.stream(parts).map(x->x[1]).toArray(String[]::new); - int maxPx = com.studentgui.uicomp.PhaseScoreField.computeMaxLabelPixelWidth(titleLabel.getFont(), labelsArr); - com.studentgui.uicomp.PhaseScoreField.setGlobalLabelWidth(Math.min(320, Math.max(140, maxPx + 50))); - skillFields = new com.studentgui.uicomp.PhaseScoreField[parts.length]; - for (int i = 0; i < parts.length; i++) { - gbc.gridy = i + 2; - gbc.gridx = 0; - gbc.gridwidth = 1; - com.studentgui.uicomp.PhaseScoreField field = new com.studentgui.uicomp.PhaseScoreField(parts[i][1], 0); - field.setName("braillenote_" + parts[i][0]); - field.getAccessibleContext().setAccessibleName(parts[i][1]); - field.setToolTipText("Enter a numeric score for " + parts[i][1]); - gbc.gridx = 0; gbc.gridwidth = 2; gbc.insets = new Insets(5, 5, 5, 5); - dataEntryPanel.add(field, gbc); - skillFields[i] = field; - gbc.gridx = 2; gbc.gridwidth = 1; gbc.insets = new Insets(5, 0, 5, 5); - dataEntryPanel.add(new JPanel(), gbc); - } - - gbc.gridy = parts.length + 3; - gbc.gridx = 0; - gbc.gridwidth = GridBagConstraints.REMAINDER; - gbc.weighty = 1.0; - dataEntryPanel.add(new JPanel(), gbc); - - gbc.gridy = parts.length + 4; - gbc.weighty = 0.0; - // layout spacing handled by PhaseScoreField - // Place Submit and Open Latest side-by-side like IOS/ScreenReader - gbc.gridy = parts.length + 4; gbc.gridx = 0; gbc.gridwidth = 1; gbc.anchor = GridBagConstraints.WEST; - JButton submitDataButton = new JButton("Submit Data"); - submitDataButton.setPreferredSize(new java.awt.Dimension(0, 32)); - submitDataButton.addActionListener((ActionEvent e) -> { submitData(); refreshGraph(); }); - submitDataButton.setMnemonic(KeyEvent.VK_S); - submitDataButton.setToolTipText("Save BrailleNote scores for the selected student (Alt+S)"); - submitDataButton.getAccessibleContext().setAccessibleName("Submit BrailleNote Data"); - dataEntryPanel.add(submitDataButton, gbc); - - gbc.gridx = 1; gbc.gridwidth = 1; gbc.anchor = GridBagConstraints.WEST; - JButton openLatestBtn = new JButton("Open Latest Plot"); - openLatestBtn.setPreferredSize(new java.awt.Dimension(0, 32)); - openLatestBtn.addActionListener((ActionEvent e) -> { - java.nio.file.Path p = com.studentgui.apphelpers.Helpers.latestPlotPath(this.studentNameParam, "BrailleNote"); - if (p == null) { - com.studentgui.apphelpers.UiNotifier.show("No BrailleNote plot found for student"); - } else { - try { - java.awt.Desktop.getDesktop().open(p.toFile()); - } catch (java.io.IOException | UnsupportedOperationException | SecurityException ex) { - com.studentgui.apphelpers.UiNotifier.show("Unable to open plot: " + p.getFileName().toString()); - } - } - }); - dataEntryPanel.add(openLatestBtn, gbc); - - add(dataEntryScrollPane, BorderLayout.CENTER); - - // Add existing graph reference - add(lineGraph, BorderLayout.SOUTH); - - SwingUtilities.invokeLater(() -> { - dataEntryPanel.setPreferredSize(dataEntryPanel.getPreferredSize()); - updateTitleDate(); - revalidate(); - }); - - // Ensure application folders and DB schema exist - com.studentgui.apphelpers.Helpers.createFolderHierarchy(); - initDatabase(); - refreshGraph(); - } - - /** - * Ensure the progress-type and assessment part rows for BrailleNote exist - * in the normalized schema. This is safe to call repeatedly. - */ - private void initDatabase() { - try { - int ptId = com.studentgui.apphelpers.Database.getOrCreateProgressType("BrailleNote"); - String[] codes = new String[this.parts.length]; - for (int i = 0; i < this.parts.length; i++) { - codes[i] = this.parts[i][0]; - } - com.studentgui.apphelpers.Database.ensureAssessmentParts(ptId, codes); - } catch (SQLException e) { - LOG.error("SQL error initializing braille note parts", e); - } - } - - /** - * Read the values entered into the skill fields and persist them to the - * database as a new progress session. Validation is performed to ensure - * numeric integer input; users are prompted on invalid values. - */ - private void submitData() { - if (this.studentNameParam == null || this.studentNameParam.trim().isEmpty()) { - JOptionPane.showMessageDialog(this, "Please select a student before submitting BrailleNote data.", "Missing student", JOptionPane.WARNING_MESSAGE); - return; - } - - try { - int studentId = com.studentgui.apphelpers.Database.getOrCreateStudent(this.studentNameParam); - int ptId = com.studentgui.apphelpers.Database.getOrCreateProgressType("BrailleNote"); - int sessionId = com.studentgui.apphelpers.Database.createProgressSession(studentId, ptId, this.dateParam); - String[] codes = new String[parts.length]; - int[] scores = new int[parts.length]; - for (int i = 0; i < parts.length && i < skillFields.length; i++) { - codes[i] = parts[i][0]; - scores[i] = skillFields[i].getValue(); - } - com.studentgui.apphelpers.Database.insertAssessmentResults(sessionId, ptId, codes, scores); - LOG.info("Data submitted successfully via normalized schema."); - com.studentgui.apphelpers.UiNotifier.show("BrailleNote data saved."); - com.studentgui.apphelpers.dto.AssessmentPayload payload = new com.studentgui.apphelpers.dto.AssessmentPayload(sessionId, codes, scores); - java.nio.file.Path jsonOut = com.studentgui.apphelpers.SessionJsonWriter.writeSessionJson(this.studentNameParam, "BrailleNote", payload, sessionId); - if (jsonOut == null) { - LOG.warn("Unable to save BrailleNote session JSON for sessionId={}", sessionId); - } - try { - java.nio.file.Path plotsOut = com.studentgui.apphelpers.Helpers.studentPlotsDir(this.studentNameParam); - java.nio.file.Path reportsOut = com.studentgui.apphelpers.Helpers.studentReportsDir(this.studentNameParam); - java.nio.file.Files.createDirectories(plotsOut); - java.nio.file.Files.createDirectories(reportsOut); - java.time.format.DateTimeFormatter df = java.time.format.DateTimeFormatter.ISO_DATE; - String dateStr = this.dateParam != null ? this.dateParam.format(df) : java.time.LocalDate.now().toString(); - String baseName = "BrailleNote-" + sessionId + "-" + dateStr; - - com.studentgui.apphelpers.Database.ResultsWithDates rwd = com.studentgui.apphelpers.Database.fetchLatestAssessmentResultsWithDates(this.studentNameParam, "BrailleNote", Integer.MAX_VALUE); - java.util.Map groups = null; - String[] labels = new String[this.parts.length]; - for (int i = 0; i < this.parts.length; i++) { - labels[i] = this.parts[i][1]; - } - if (rwd != null && rwd.rows != null && !rwd.rows.isEmpty()) { - lineGraph.updateWithGroupedDataByDate(rwd.dates, rwd.rows, codes, labels); - groups = lineGraph.saveGroupedCharts(plotsOut, baseName, 1000, 240); - java.time.LocalDate headerDate = rwd.dates.get(rwd.dates.size() - 1); - dateStr = headerDate.format(df); - } else { - java.util.List> rowsList = new java.util.ArrayList<>(); - java.util.List latest = new java.util.ArrayList<>(); - for (int v : scores) { - latest.add(v); - } - rowsList.add(latest); - lineGraph.updateWithGroupedData(rowsList, codes); - groups = lineGraph.saveGroupedCharts(plotsOut, baseName, 1000, 240); - } - - if (groups == null) { - groups = new java.util.LinkedHashMap<>(); - } - StringBuilder md = new StringBuilder(); - md.append("# ").append(this.studentNameParam == null ? "Unknown Student" : this.studentNameParam).append(" - ").append(dateStr).append("\n\n"); - for (java.util.Map.Entry e : groups.entrySet()) { - md.append("## ").append(e.getKey()).append("\n\n"); - md.append("![](./").append(e.getValue().getFileName().toString()).append(")\n\n"); - } - java.nio.file.Path mdFile = reportsOut.resolve(baseName + ".md"); - String mdText = md.toString().replace("![](./", "![](../plots/"); - java.nio.file.Files.writeString(mdFile, mdText, java.nio.charset.StandardCharsets.UTF_8); - - try { - String[] palette = JLineGraph.PALETTE_HEX; - java.util.LinkedHashMap> groupsIdx = new java.util.LinkedHashMap<>(); - for (int i = 0; i < codes.length; i++) { - String code = codes[i]; - String grp = code != null && code.contains("_") ? code.split("_")[0] : code; - groupsIdx.computeIfAbsent(grp, k -> new java.util.ArrayList<>()).add(i); - } - StringBuilder html = new StringBuilder(); - html.append(""); - html.append(this.studentNameParam == null ? "Student Report" : this.studentNameParam).append(" - ").append(dateStr).append(""); - html.append(""); - html.append(""); - html.append("

").append(this.studentNameParam == null ? "Unknown Student" : this.studentNameParam).append(" - ").append(dateStr).append("

"); - for (java.util.Map.Entry e2 : groups.entrySet()) { - String grp = e2.getKey(); - String imgName = e2.getValue().getFileName().toString(); - html.append("

").append(grp).append("

"); - html.append("
\"").append(grp).append("\"
"); - java.util.List idxs = groupsIdx.getOrDefault(grp, new java.util.ArrayList<>()); - html.append("
"); - for (int s = 0; s < idxs.size(); s++) { - int idx = idxs.get(s); - String code = codes[idx]; - String human = this.parts[idx][1]; - String seriesName = code + " - " + human; - String color = palette[s % palette.length]; - html.append("
"); - html.append(""); - html.append("
"); - html.append(seriesName); - html.append("
"); - } - html.append("
"); - } - html.append(""); - java.nio.file.Path htmlFile = reportsOut.resolve(baseName + ".html"); - String htmlStr = html.toString().replace("src=\"./", "src=\"../plots/"); - java.nio.file.Files.writeString(htmlFile, htmlStr, java.nio.charset.StandardCharsets.UTF_8); - LOG.info("Wrote BrailleNote HTML session report {}", htmlFile); - } catch (java.io.IOException ioex) { - LOG.warn("Unable to write BrailleNote HTML report: {}", ioex.toString()); - } - } catch (java.io.IOException ioe) { - LOG.warn("Unable to save BrailleNote per-phase plots or markdown report: {}", ioe.toString()); - } - } catch (SQLException e) { - LOG.error("SQL error saving braille note data", e); - JOptionPane.showMessageDialog(this, "Database error saving BrailleNote data: " + e.getMessage(), "Database error", JOptionPane.ERROR_MESSAGE); - } - } - - /** - * Query the most recent assessment sessions for this student and update - * the shared {@link JLineGraph} with the returned values. - */ - private void refreshGraph() { - try { - List> allSkillValues = com.studentgui.apphelpers.Database.fetchLatestAssessmentResults(studentNameParam, "BrailleNote", 5); - if (allSkillValues != null && !allSkillValues.isEmpty()) { - String[] codes = new String[this.parts.length]; - for (int i = 0; i < this.parts.length; i++) { - codes[i] = this.parts[i][0]; - } - lineGraph.updateWithGroupedData(allSkillValues, codes); - // Write to the consolidated per-run data dumps file when enabled - if (Boolean.parseBoolean(com.studentgui.apphelpers.Settings.get("dump.enabled", "false"))) { - try { - String appHome = System.getProperty("APP_HOME", com.studentgui.apphelpers.Helpers.APP_HOME.toString()); - String ts = System.getProperty("LOG_TS", String.valueOf(java.time.Instant.now().getEpochSecond())); - java.nio.file.Path logDir = java.nio.file.Paths.get(appHome).resolve("logs"); - java.nio.file.Files.createDirectories(logDir); - java.nio.file.Path logFile = logDir.resolve("data_dumps_" + ts + ".log"); - StringBuilder sb = new StringBuilder(); - sb.append("[BrailleNote]").append(System.lineSeparator()); - sb.append(java.time.Instant.now().toString()).append(" - student=").append(this.studentNameParam).append(System.lineSeparator()); - sb.append("data=").append(allSkillValues.toString()).append(System.lineSeparator()); - sb.append(System.lineSeparator()); - java.nio.file.Files.writeString(logFile, sb.toString(), java.nio.charset.StandardCharsets.UTF_8, java.nio.file.StandardOpenOption.CREATE, java.nio.file.StandardOpenOption.APPEND); - } catch (java.io.IOException ioe) { - LOG.trace("Unable to write BrailleNote load log: {}", ioe.toString()); - } - } - } else { - LOG.info("No data to plot."); - // Ensure the graph shows grouped placeholders matching the - // canonical assessment part ordering so the UI displays - // one subchart per P# prefix even with no sessions. - String[] codes = new String[this.parts.length]; - for (int i = 0; i < this.parts.length; i++) { - codes[i] = this.parts[i][0]; - } - lineGraph.showEmptyGrouped(codes); - } - } catch (SQLException e) { - LOG.error("SQL error refreshing braille note graph", e); - } - } - - @Override - public void dateChanged(LocalDate newDate) { - this.dateParam = newDate; - SwingUtilities.invokeLater(() -> { - refreshGraph(); - updateTitleDate(); - }); - } - - @Override - public void studentChanged(String newStudent) { - this.studentNameParam = newStudent; - SwingUtilities.invokeLater(() -> { - refreshGraph(); - updateTitleDate(); - }); - } - - private void updateTitleDate() { - try { - String dateStr = this.dateParam != null ? this.dateParam.toString() : java.time.LocalDate.now().toString(); - this.titleLabel.setText(baseTitle + " - " + dateStr); - } catch (Exception ex) { - this.titleLabel.setText(baseTitle); - } - } - - -} +package com.studentgui.apppages; +import java.awt.BorderLayout; +import java.awt.Font; +import java.awt.GridBagConstraints; +import java.awt.GridBagLayout; +import java.awt.Insets; +import java.awt.event.ActionEvent; +import java.awt.event.KeyEvent; +import java.sql.SQLException; +import java.time.LocalDate; +import java.util.List; + +import javax.swing.JButton; +import javax.swing.JLabel; +import javax.swing.JOptionPane; +import javax.swing.JPanel; +import javax.swing.JScrollPane; +import javax.swing.SwingUtilities; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + * HumanWare BrailleNote Touch Plus (BNT+) proficiency assessment page. + * + *

Evaluates student competency with the BrailleNote Touch Plus refreshable braille notetaker + * and productivity device across 52 skills organized into 12 functional domains:

+ * + *
    + *
  • Phase 1 (P1_1–P1_9): Device Fundamentals and Core Applications + *
      + *
    • Physical layout (braille keyboard, navigation keys, touchscreen, ports)
    • + *
    • Setup procedures and universal commands (power, mode switching, context menus)
    • + *
    • BNT+ navigation paradigm (gestures, quick keys, braille commands)
    • + *
    • File management (folders, copy/paste, rename, delete)
    • + *
    • Word processor (KeyWord): document creation, editing, formatting
    • + *
    • Email (KeyMail): compose, send, receive, attachments
    • + *
    • Internet browsing (KeyWeb): navigation, bookmarks, forms
    • + *
    • Calculator and KeyMath (arithmetic, scientific functions)
    • + *
    + *
  • + *
  • Phase 2 (P2_1–P2_7): Productivity Suite Applications + *
      + *
    • Calendar management (appointments, reminders, recurring events)
    • + *
    • KeyBRF (Braille file viewer/editor)
    • + *
    • KeyFiles (file explorer and organizer)
    • + *
    • KeyMail (advanced email features)
    • + *
    • KeyWeb (advanced browsing, accessibility modes)
    • + *
    • KeyCalc (spreadsheet concepts)
    • + *
    • KeyWord (advanced formatting, styles, tables)
    • + *
    + *
  • + *
  • Phase 3 (P3_1–P3_7): Advanced Applications and Accessibility + *
      + *
    • KeySlides (presentation creation and delivery)
    • + *
    • KeyCode (text editor with syntax highlighting for programming)
    • + *
    • Third-party app integration (Dropbox, Google Drive, OneDrive)
    • + *
    • Braille input configuration (computer braille, contracted, literary)
    • + *
    • Braille output settings (display mode, translation tables)
    • + *
    • Device settings and preferences
    • + *
    • Accessibility features (speech output, magnification, contrast)
    • + *
    + *
  • + *
  • Phase 4 (P4_1–P4_3): Advanced File and Cloud Management
  • + *
  • Phase 5 (P5_1–P5_4): Collaboration and Export Workflows
  • + *
  • Phase 6 (P6_1–P6_3): App Ecosystem and Troubleshooting
  • + *
  • Phase 7 (P7_1–P7_4): Automation and Customization
  • + *
  • Phase 8 (P8_1–P8_5): Peripheral Integration (Bluetooth/USB devices, displays, audio/video)
  • + *
  • Phase 9 (P9_1–P9_4): Security and Network Configuration
  • + *
  • Phase 10 (P10_1–P10_3): Speech Engine Customization
  • + *
  • Phase 11 (P11_1–P11_5): Maintenance and Support (firmware, diagnostics, warranty)
  • + *
  • Phase 12 (P12_1–P12_4): Community and Online Resources
  • + *
+ * + *

Data Management and Artifacts:

+ *
    + *
  • Scores captured via {@link com.studentgui.uicomp.PhaseScoreField} (integer 0–4 typical)
  • + *
  • Persisted to normalized schema via {@link com.studentgui.apphelpers.Database#insertAssessmentResults}
  • + *
  • JSON export: {@code StudentDataFiles//Sessions/BrailleNote/BrailleNote--.json}
  • + *
  • Phase-grouped time-series plots: {@code plots/BrailleNote---P.png} (12 phase groups)
  • + *
  • Markdown and HTML reports with embedded plots and color-coded legends
  • + *
+ * + *

The shared {@link JLineGraph} visualizes recent session trends grouped by phase prefix. + * Implements {@link com.studentgui.app.DateChangeListener} and {@link com.studentgui.app.StudentChangeListener} + * for dynamic updates when global student/date selections change.

+ * + * @see com.studentgui.apphelpers.Database + * @see JLineGraph + * @see com.studentgui.uicomp.PhaseScoreField + */ +public class BrailleNote extends JPanel implements com.studentgui.app.DateChangeListener, com.studentgui.app.StudentChangeListener { + private static final Logger LOG = LoggerFactory.getLogger(BrailleNote.class); + + /** Inputs for each BrailleNote skill. */ + private final com.studentgui.uicomp.PhaseScoreField[] skillFields; + /** Canonical assessment part codes and labels for BrailleNote. */ + private final String[][] parts; + /** Shared graph component for plotting results. */ + private final JLineGraph lineGraph; // Reference to the JLineGraph instance + /** Display name of the selected student (may be null). */ + private String studentNameParam; + /** Header title label for this page. */ + private JLabel titleLabel; + /** Base page title string used when rendering the header (date appended). */ + private final String baseTitle = "BrailleNote Skills Progression"; + /** Session date associated with persisted progress. */ + private LocalDate dateParam; + + /** + * Create the BrailleNote page for a specific student and date. + * + * @param studentName the selected student name (may be null until a student is chosen) + * @param date the date for the session (used when creating a progress session) + * @param lineGraph shared graph component used to display recent results + */ + public BrailleNote(String studentName, LocalDate date, JLineGraph lineGraph) { + this.studentNameParam = (studentName == null || studentName.trim().isEmpty()) ? com.studentgui.apphelpers.Helpers.defaultStudent() : studentName; + this.dateParam = date; + this.lineGraph = lineGraph; // Use the passed in graph instance + setLayout(new BorderLayout()); + + this.parts = new String[][]{ + {"P1_1","1.1 Physical Layout"},{"P1_2","1.2 Setup/Universal Commands"},{"P1_3","1.3 BNT+ Navigation"},{"P1_4","1.4 File Management"},{"P1_5","1.5 Word Processor"},{"P1_6","1.6 Email"},{"P1_7","1.7 Internet"},{"P1_8","1.8 Calculator"},{"P1_9","1.9 KeyMath"}, + {"P2_1","2.1 Calendar"},{"P2_2","2.2 KeyBRF"},{"P2_3","2.3 KeyFiles"},{"P2_4","2.4 KeyMail"},{"P2_5","2.5 KeyWeb"},{"P2_6","2.6 KeyCalc"},{"P2_7","2.7 KeyWord"}, + {"P3_1","3.1 KeySlides"},{"P3_2","3.2 KeyCode"},{"P3_3","3.3 Third Party Apps"},{"P3_4","3.4 Braille Input"},{"P3_5","3.5 Braille Output"},{"P3_6","3.6 Settings"},{"P3_7","3.7 Accessibility"}, + {"P4_1","4.1 Advanced File Management"},{"P4_2","4.2 Cloud Integration"},{"P4_3","4.3 Device Maintenance"}, + {"P5_1","5.1 Collaboration"},{"P5_2","5.2 Export/Import"},{"P5_3","5.3 Printing"},{"P5_4","5.4 Backup"}, + {"P6_1","6.1 App Installation"},{"P6_2","6.2 App Updates"},{"P6_3","6.3 Troubleshooting"}, + {"P7_1","7.1 Custom Shortcuts"},{"P7_2","7.2 Macros"},{"P7_3","7.3 Scripting"},{"P7_4","7.4 Automation"}, + {"P8_1","8.1 Bluetooth Devices"},{"P8_2","8.2 USB Devices"},{"P8_3","8.3 External Displays"},{"P8_4","8.4 Audio Output"},{"P8_5","8.5 Video Output"}, + {"P9_1","9.1 Security"},{"P9_2","9.2 User Accounts"},{"P9_3","9.3 Parental Controls"},{"P9_4","9.4 Network Settings"}, + {"P10_1","10.1 Speech Settings"},{"P10_2","10.2 Voice Profiles"},{"P10_3","10.3 Language Support"}, + {"P11_1","11.1 Firmware Updates"},{"P11_2","11.2 Diagnostics"},{"P11_3","11.3 Logs"},{"P11_4","11.4 Support"},{"P11_5","11.5 Warranty"}, + {"P12_1","12.1 Community Resources"},{"P12_2","12.2 Online Help"},{"P12_3","12.3 User Forums"},{"P12_4","12.4 Feedback"} + }; + + // Panel for data entry + JPanel dataEntryPanel = new JPanel(); + dataEntryPanel.setLayout(new GridBagLayout()); + JScrollPane dataEntryScrollPane = new JScrollPane(dataEntryPanel); + dataEntryScrollPane.setVerticalScrollBarPolicy(JScrollPane.VERTICAL_SCROLLBAR_AS_NEEDED); + dataEntryScrollPane.setHorizontalScrollBarPolicy(JScrollPane.HORIZONTAL_SCROLLBAR_NEVER); + dataEntryScrollPane.getAccessibleContext().setAccessibleName("BrailleNote data entry scroll pane"); + + GridBagConstraints gbc = new GridBagConstraints(); + gbc.insets = new Insets(5, 5, 5, 5); + gbc.fill = GridBagConstraints.HORIZONTAL; + gbc.weightx = 1.0; + gbc.weighty = 0.0; + + this.titleLabel = new JLabel(baseTitle); + this.titleLabel.setFont(this.titleLabel.getFont().deriveFont(Font.BOLD, 16)); + gbc.gridx = 0; + gbc.gridy = 0; + gbc.gridwidth = GridBagConstraints.REMAINDER; + dataEntryPanel.add(this.titleLabel, gbc); + + gbc.gridy = 1; + gbc.gridwidth = GridBagConstraints.REMAINDER; + gbc.ipady = 20; + dataEntryPanel.add(new JPanel(), gbc); + + // layout spacing handled by PhaseScoreField + + // compute pixel width using font metrics so labels align precisely + String[] labelsArr = java.util.Arrays.stream(parts).map(x->x[1]).toArray(String[]::new); + int maxPx = com.studentgui.uicomp.PhaseScoreField.computeMaxLabelPixelWidth(titleLabel.getFont(), labelsArr); + com.studentgui.uicomp.PhaseScoreField.setGlobalLabelWidth(Math.min(320, Math.max(140, maxPx + 50))); + skillFields = new com.studentgui.uicomp.PhaseScoreField[parts.length]; + for (int i = 0; i < parts.length; i++) { + gbc.gridy = i + 2; + gbc.gridx = 0; + gbc.gridwidth = 1; + com.studentgui.uicomp.PhaseScoreField field = new com.studentgui.uicomp.PhaseScoreField(parts[i][1], 0); + field.setName("braillenote_" + parts[i][0]); + field.getAccessibleContext().setAccessibleName(parts[i][1]); + field.setToolTipText("Enter a numeric score for " + parts[i][1]); + gbc.gridx = 0; gbc.gridwidth = 2; gbc.insets = new Insets(5, 5, 5, 5); + dataEntryPanel.add(field, gbc); + skillFields[i] = field; + gbc.gridx = 2; gbc.gridwidth = 1; gbc.insets = new Insets(5, 0, 5, 5); + dataEntryPanel.add(new JPanel(), gbc); + } + + gbc.gridy = parts.length + 3; + gbc.gridx = 0; + gbc.gridwidth = GridBagConstraints.REMAINDER; + gbc.weighty = 1.0; + dataEntryPanel.add(new JPanel(), gbc); + + gbc.gridy = parts.length + 4; + gbc.weighty = 0.0; + // layout spacing handled by PhaseScoreField + // Place Submit and Open Latest side-by-side like IOS/ScreenReader + gbc.gridy = parts.length + 4; gbc.gridx = 0; gbc.gridwidth = 1; gbc.anchor = GridBagConstraints.WEST; + JButton submitDataButton = new JButton("Submit Data"); + submitDataButton.setPreferredSize(new java.awt.Dimension(0, 32)); + submitDataButton.addActionListener((ActionEvent e) -> { submitData(); refreshGraph(); }); + submitDataButton.setMnemonic(KeyEvent.VK_S); + submitDataButton.setToolTipText("Save BrailleNote scores for the selected student (Alt+S)"); + submitDataButton.getAccessibleContext().setAccessibleName("Submit BrailleNote Data"); + dataEntryPanel.add(submitDataButton, gbc); + + gbc.gridx = 1; gbc.gridwidth = 1; gbc.anchor = GridBagConstraints.WEST; + JButton openLatestBtn = new JButton("Open Latest Plot"); + openLatestBtn.setPreferredSize(new java.awt.Dimension(0, 32)); + openLatestBtn.addActionListener((ActionEvent e) -> { + java.nio.file.Path p = com.studentgui.apphelpers.Helpers.latestPlotPath(this.studentNameParam, "BrailleNote"); + if (p == null) { + com.studentgui.apphelpers.UiNotifier.show("No BrailleNote plot found for student"); + } else { + try { + java.awt.Desktop.getDesktop().open(p.toFile()); + } catch (java.io.IOException | UnsupportedOperationException | SecurityException ex) { + com.studentgui.apphelpers.UiNotifier.show("Unable to open plot: " + p.getFileName().toString()); + } + } + }); + dataEntryPanel.add(openLatestBtn, gbc); + + add(dataEntryScrollPane, BorderLayout.CENTER); + + // Add existing graph reference + add(lineGraph, BorderLayout.SOUTH); + + SwingUtilities.invokeLater(() -> { + dataEntryPanel.setPreferredSize(dataEntryPanel.getPreferredSize()); + updateTitleDate(); + revalidate(); + }); + + // Ensure application folders and DB schema exist + com.studentgui.apphelpers.Helpers.createFolderHierarchy(); + initDatabase(); + refreshGraph(); + } + + /** + * Ensure the progress-type and assessment part rows for BrailleNote exist + * in the normalized schema. This is safe to call repeatedly. + */ + private void initDatabase() { + try { + int ptId = com.studentgui.apphelpers.Database.getOrCreateProgressType("BrailleNote"); + String[] codes = new String[this.parts.length]; + for (int i = 0; i < this.parts.length; i++) { + codes[i] = this.parts[i][0]; + } + com.studentgui.apphelpers.Database.ensureAssessmentParts(ptId, codes); + } catch (SQLException e) { + LOG.error("SQL error initializing braille note parts", e); + } + } + + /** + * Read the values entered into the skill fields and persist them to the + * database as a new progress session. Validation is performed to ensure + * numeric integer input; users are prompted on invalid values. + */ + private void submitData() { + if (this.studentNameParam == null || this.studentNameParam.trim().isEmpty()) { + JOptionPane.showMessageDialog(this, "Please select a student before submitting BrailleNote data.", "Missing student", JOptionPane.WARNING_MESSAGE); + return; + } + + try { + int studentId = com.studentgui.apphelpers.Database.getOrCreateStudent(this.studentNameParam); + int ptId = com.studentgui.apphelpers.Database.getOrCreateProgressType("BrailleNote"); + int sessionId = com.studentgui.apphelpers.Database.createProgressSession(studentId, ptId, this.dateParam); + String[] codes = new String[parts.length]; + int[] scores = new int[parts.length]; + for (int i = 0; i < parts.length && i < skillFields.length; i++) { + codes[i] = parts[i][0]; + scores[i] = skillFields[i].getValue(); + } + com.studentgui.apphelpers.Database.insertAssessmentResults(sessionId, ptId, codes, scores); + LOG.info("Data submitted successfully via normalized schema."); + com.studentgui.apphelpers.UiNotifier.show("BrailleNote data saved."); + com.studentgui.apphelpers.dto.AssessmentPayload payload = new com.studentgui.apphelpers.dto.AssessmentPayload(sessionId, codes, scores); + java.nio.file.Path jsonOut = com.studentgui.apphelpers.SessionJsonWriter.writeSessionJson(this.studentNameParam, "BrailleNote", payload, sessionId); + if (jsonOut == null) { + LOG.warn("Unable to save BrailleNote session JSON for sessionId={}", sessionId); + } + try { + java.nio.file.Path plotsOut = com.studentgui.apphelpers.Helpers.studentPlotsDir(this.studentNameParam); + java.nio.file.Path reportsOut = com.studentgui.apphelpers.Helpers.studentReportsDir(this.studentNameParam); + java.nio.file.Files.createDirectories(plotsOut); + java.nio.file.Files.createDirectories(reportsOut); + java.time.format.DateTimeFormatter df = java.time.format.DateTimeFormatter.ISO_DATE; + String dateStr = this.dateParam != null ? this.dateParam.format(df) : java.time.LocalDate.now().toString(); + String baseName = "BrailleNote-" + sessionId + "-" + dateStr; + + com.studentgui.apphelpers.Database.ResultsWithDates rwd = com.studentgui.apphelpers.Database.fetchLatestAssessmentResultsWithDates(this.studentNameParam, "BrailleNote", Integer.MAX_VALUE); + java.util.Map groups = null; + String[] labels = new String[this.parts.length]; + for (int i = 0; i < this.parts.length; i++) { + labels[i] = this.parts[i][1]; + } + if (rwd != null && rwd.rows != null && !rwd.rows.isEmpty()) { + lineGraph.updateWithGroupedDataByDate(rwd.dates, rwd.rows, codes, labels); + groups = lineGraph.saveGroupedCharts(plotsOut, baseName, 1000, 240); + java.time.LocalDate headerDate = rwd.dates.get(rwd.dates.size() - 1); + dateStr = headerDate.format(df); + } else { + java.util.List> rowsList = new java.util.ArrayList<>(); + java.util.List latest = new java.util.ArrayList<>(); + for (int v : scores) { + latest.add(v); + } + rowsList.add(latest); + lineGraph.updateWithGroupedData(rowsList, codes); + groups = lineGraph.saveGroupedCharts(plotsOut, baseName, 1000, 240); + } + + if (groups == null) { + groups = new java.util.LinkedHashMap<>(); + } + StringBuilder md = new StringBuilder(); + md.append("# ").append(this.studentNameParam == null ? "Unknown Student" : this.studentNameParam).append(" - ").append(dateStr).append("\n\n"); + for (java.util.Map.Entry e : groups.entrySet()) { + md.append("## ").append(e.getKey()).append("\n\n"); + md.append("![](./").append(e.getValue().getFileName().toString()).append(")\n\n"); + } + java.nio.file.Path mdFile = reportsOut.resolve(baseName + ".md"); + String mdText = md.toString().replace("![](./", "![](../plots/"); + java.nio.file.Files.writeString(mdFile, mdText, java.nio.charset.StandardCharsets.UTF_8); + + try { + String[] palette = JLineGraph.PALETTE_HEX; + java.util.LinkedHashMap> groupsIdx = new java.util.LinkedHashMap<>(); + for (int i = 0; i < codes.length; i++) { + String code = codes[i]; + String grp = code != null && code.contains("_") ? code.split("_")[0] : code; + groupsIdx.computeIfAbsent(grp, k -> new java.util.ArrayList<>()).add(i); + } + StringBuilder html = new StringBuilder(); + html.append(""); + html.append(this.studentNameParam == null ? "Student Report" : this.studentNameParam).append(" - ").append(dateStr).append(""); + html.append(""); + html.append(""); + html.append("

").append(this.studentNameParam == null ? "Unknown Student" : this.studentNameParam).append(" - ").append(dateStr).append("

"); + for (java.util.Map.Entry e2 : groups.entrySet()) { + String grp = e2.getKey(); + String imgName = e2.getValue().getFileName().toString(); + html.append("

").append(grp).append("

"); + html.append("
\"").append(grp).append("\"
"); + java.util.List idxs = groupsIdx.getOrDefault(grp, new java.util.ArrayList<>()); + html.append("
"); + for (int s = 0; s < idxs.size(); s++) { + int idx = idxs.get(s); + String code = codes[idx]; + String human = this.parts[idx][1]; + String seriesName = code + " - " + human; + String color = palette[s % palette.length]; + html.append("
"); + html.append(""); + html.append("
"); + html.append(seriesName); + html.append("
"); + } + html.append("
"); + } + html.append(""); + java.nio.file.Path htmlFile = reportsOut.resolve(baseName + ".html"); + String htmlStr = html.toString().replace("src=\"./", "src=\"../plots/"); + java.nio.file.Files.writeString(htmlFile, htmlStr, java.nio.charset.StandardCharsets.UTF_8); + LOG.info("Wrote BrailleNote HTML session report {}", htmlFile); + } catch (java.io.IOException ioex) { + LOG.warn("Unable to write BrailleNote HTML report: {}", ioex.toString()); + } + } catch (java.io.IOException ioe) { + LOG.warn("Unable to save BrailleNote per-phase plots or markdown report: {}", ioe.toString()); + } + } catch (SQLException e) { + LOG.error("SQL error saving braille note data", e); + JOptionPane.showMessageDialog(this, "Database error saving BrailleNote data: " + e.getMessage(), "Database error", JOptionPane.ERROR_MESSAGE); + } + } + + /** + * Query the most recent assessment sessions for this student and update + * the shared {@link JLineGraph} with the returned values. + */ + private void refreshGraph() { + try { + List> allSkillValues = com.studentgui.apphelpers.Database.fetchLatestAssessmentResults(studentNameParam, "BrailleNote", 5); + if (allSkillValues != null && !allSkillValues.isEmpty()) { + String[] codes = new String[this.parts.length]; + for (int i = 0; i < this.parts.length; i++) { + codes[i] = this.parts[i][0]; + } + lineGraph.updateWithGroupedData(allSkillValues, codes); + // Write to the consolidated per-run data dumps file when enabled + if (Boolean.parseBoolean(com.studentgui.apphelpers.Settings.get("dump.enabled", "false"))) { + try { + String appHome = System.getProperty("APP_HOME", com.studentgui.apphelpers.Helpers.APP_HOME.toString()); + String ts = System.getProperty("LOG_TS", String.valueOf(java.time.Instant.now().getEpochSecond())); + java.nio.file.Path logDir = java.nio.file.Paths.get(appHome).resolve("logs"); + java.nio.file.Files.createDirectories(logDir); + java.nio.file.Path logFile = logDir.resolve("data_dumps_" + ts + ".log"); + StringBuilder sb = new StringBuilder(); + sb.append("[BrailleNote]").append(System.lineSeparator()); + sb.append(java.time.Instant.now().toString()).append(" - student=").append(this.studentNameParam).append(System.lineSeparator()); + sb.append("data=").append(allSkillValues.toString()).append(System.lineSeparator()); + sb.append(System.lineSeparator()); + java.nio.file.Files.writeString(logFile, sb.toString(), java.nio.charset.StandardCharsets.UTF_8, java.nio.file.StandardOpenOption.CREATE, java.nio.file.StandardOpenOption.APPEND); + } catch (java.io.IOException ioe) { + LOG.trace("Unable to write BrailleNote load log: {}", ioe.toString()); + } + } + } else { + LOG.info("No data to plot."); + // Ensure the graph shows grouped placeholders matching the + // canonical assessment part ordering so the UI displays + // one subchart per P# prefix even with no sessions. + String[] codes = new String[this.parts.length]; + for (int i = 0; i < this.parts.length; i++) { + codes[i] = this.parts[i][0]; + } + lineGraph.showEmptyGrouped(codes); + } + } catch (SQLException e) { + LOG.error("SQL error refreshing braille note graph", e); + } + } + + @Override + public void dateChanged(LocalDate newDate) { + this.dateParam = newDate; + SwingUtilities.invokeLater(() -> { + refreshGraph(); + updateTitleDate(); + }); + } + + @Override + public void studentChanged(String newStudent) { + this.studentNameParam = newStudent; + SwingUtilities.invokeLater(() -> { + refreshGraph(); + updateTitleDate(); + }); + } + + private void updateTitleDate() { + try { + String dateStr = this.dateParam != null ? this.dateParam.toString() : java.time.LocalDate.now().toString(); + this.titleLabel.setText(baseTitle + " - " + dateStr); + } catch (Exception ex) { + this.titleLabel.setText(baseTitle); + } + } + + +} diff --git a/src/main/java/com/studentgui/apppages/BrailleSense.java b/src/main/java/com/studentgui/apppages/BrailleSense.java index 2e840a5..5504965 100644 --- a/src/main/java/com/studentgui/apppages/BrailleSense.java +++ b/src/main/java/com/studentgui/apppages/BrailleSense.java @@ -1,308 +1,346 @@ -package com.studentgui.apppages; - -import java.awt.BorderLayout; -import java.awt.Font; -import java.awt.GridBagConstraints; -import java.awt.GridBagLayout; -import java.awt.Insets; -import java.awt.event.ActionEvent; -import java.awt.event.KeyEvent; -import java.sql.SQLException; -import java.time.LocalDate; -import java.util.LinkedHashMap; -import java.util.Map; - -import javax.swing.JButton; -import javax.swing.JLabel; -import javax.swing.JPanel; -import javax.swing.JScrollPane; -import javax.swing.SwingUtilities; - -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; - -import com.studentgui.uicomp.PhaseScoreField; - -/** - * BrailleSense skills progression UI page. - *

- * Presents a compact set of inputs keyed by part code (e.g. P1_1) and allows - * saving those values into the canonical database schema. A shared - * {@link JLineGraph} instance is used to visualize recent results. - *

- */ -public class BrailleSense extends JPanel { - private static final Logger LOG = LoggerFactory.getLogger(BrailleSense.class); - /** Map of assessment part codes to their input components. */ - private final Map inputs = new LinkedHashMap<>(); - /** Canonical assessment parts for BrailleSense. */ - private final String[][] parts; - /** Selected student display name (may be null). */ - private final String studentNameParam; - /** Date associated with the current session. */ - private final LocalDate dateParam; - /** Shared graph component used to visualize recent results. */ - private final JLineGraph graph; - - /** - * Create a BrailleSense page bound to the provided student and date. - * - * @param studentName selected student name (may be null until selection) - * @param date session date to associate with persisted progress rows - * @param graph shared graph component used to plot recent results - */ - public BrailleSense(String studentName, LocalDate date, JLineGraph graph) { - this.studentNameParam = (studentName == null || studentName.trim().isEmpty()) ? com.studentgui.apphelpers.Helpers.defaultStudent() : studentName; - this.dateParam = date; - this.graph = graph; - setLayout(new BorderLayout()); - - // create a data entry panel that mirrors BrailleNote's layout so alignment is identical - JPanel dataEntryPanel = new JPanel(new GridBagLayout()); - JPanel view = new JPanel(new BorderLayout()); - view.add(dataEntryPanel, BorderLayout.NORTH); - view.setBorder(javax.swing.BorderFactory.createEmptyBorder(20, 20, 20, 20)); - JScrollPane dataEntryScrollPane = new JScrollPane(view); - dataEntryScrollPane.setVerticalScrollBarPolicy(javax.swing.JScrollPane.VERTICAL_SCROLLBAR_AS_NEEDED); - dataEntryScrollPane.setHorizontalScrollBarPolicy(javax.swing.JScrollPane.HORIZONTAL_SCROLLBAR_NEVER); - dataEntryScrollPane.getAccessibleContext().setAccessibleName("BrailleSense data entry scroll pane"); - - GridBagConstraints gbc = new GridBagConstraints(); - gbc.insets = new Insets(2, 2, 2, 2); - gbc.fill = GridBagConstraints.HORIZONTAL; - gbc.weightx = 1.0; - gbc.weighty = 0.0; - - JLabel titleLabel = new JLabel("BrailleSense Skills"); - // Use an explicit font so theme changes don't alter the title appearance - titleLabel.setFont(new Font(Font.SANS_SERIF, Font.BOLD, 16)); - gbc.gridx = 0; - gbc.gridy = 0; - gbc.gridwidth = GridBagConstraints.REMAINDER; - dataEntryPanel.add(titleLabel, gbc); - - gbc.gridy = 1; - gbc.gridwidth = GridBagConstraints.REMAINDER; - gbc.ipady = 20; - dataEntryPanel.add(new JPanel(), gbc); - - this.parts = new String[][]{ - {"P1_1", "1.1 Physical Layout"}, {"P1_2", "1.2 Setup/Universal Commands"}, {"P1_3", "1.3 BNT+ Navigation"}, {"P1_4", "1.4 File Management"}, {"P1_5", "1.5 Word Processor"}, {"P1_6", "1.6 Email"}, {"P1_7", "1.7 Internet"}, {"P1_8", "1.8 Calculator"}, {"P1_9", "1.9 KeyMath"}, - {"P2_1", "2.1 Calendar"}, {"P2_2", "2.2 KeyBRF"}, {"P2_3", "2.3 KeyFiles"}, {"P2_4", "2.4 KeyMail"}, {"P2_5", "2.5 KeyWeb"}, {"P2_6", "2.6 KeyCalc"}, {"P2_7", "2.7 KeyWord"}, - {"P3_1", "3.1 KeySlides"}, {"P3_2", "3.2 KeyCode"}, {"P3_3", "3.3 Third Party Apps"}, {"P3_4", "3.4 Braille Input"}, {"P3_5", "3.5 Braille Output"}, {"P3_6", "3.6 Settings"}, {"P3_7", "3.7 Accessibility"}, - {"P4_1", "4.1 Advanced File Management"}, {"P4_2", "4.2 Cloud Integration"}, {"P4_3", "4.3 Device Maintenance"}, - {"P5_1", "5.1 Collaboration"}, {"P5_2", "5.2 Export/Import"}, {"P5_3", "5.3 Printing"}, {"P5_4", "5.4 Backup"}, - {"P6_1", "6.1 App Installation"}, {"P6_2", "6.2 App Updates"}, {"P6_3", "6.3 Troubleshooting"}, - {"P7_1", "7.1 Custom Shortcuts"}, {"P7_2", "7.2 Macros"}, {"P7_3", "7.3 Scripting"}, {"P7_4", "7.4 Automation"}, - {"P8_1", "8.1 Bluetooth Devices"}, {"P8_2", "8.2 USB Devices"}, {"P8_3", "8.3 External Displays"}, {"P8_4", "8.4 Audio Output"}, {"P8_5", "8.5 Video Output"}, - {"P9_1", "9.1 Security"}, {"P9_2", "9.2 User Accounts"}, {"P9_3", "9.3 Parental Controls"}, {"P9_4", "9.4 Network Settings"}, - {"P10_1", "10.1 Speech Settings"}, {"P10_2", "10.2 Voice Profiles"}, {"P10_3", "10.3 Language Support"}, - {"P11_1", "11.1 Firmware Updates"}, {"P11_2", "11.2 Diagnostics"}, {"P11_3", "11.3 Logs"}, {"P11_4", "11.4 Support"}, {"P11_5", "11.5 Warranty"}, - {"P12_1", "12.1 Community Resources"}, {"P12_2", "12.2 Online Help"}, {"P12_3", "12.3 User Forums"}, {"P12_4", "12.4 Feedback"} - }; - - // compute pixel width using font metrics so labels align precisely - String[] labels = java.util.Arrays.stream(this.parts).map(x -> x[1]).toArray(String[]::new); - int maxPx = com.studentgui.uicomp.PhaseScoreField.computeMaxLabelPixelWidth(titleLabel.getFont(), labels); - com.studentgui.uicomp.PhaseScoreField.setGlobalLabelWidth(Math.min(360, Math.max(200, maxPx + 50))); - int row = 1; - for (String[] def : this.parts) { - gbc.gridx = 0; - gbc.gridy = row; - gbc.gridwidth = 2; - PhaseScoreField tf = new PhaseScoreField(def[1], 0); - tf.setName("braillesense_" + def[0]); - tf.getAccessibleContext().setAccessibleName(def[1]); - tf.setToolTipText("Enter score for " + def[1]); - dataEntryPanel.add(tf, gbc); - inputs.put(def[0], tf); - row++; - } - - // Place Submit and Open Latest side-by-side to match IOS/ScreenReader styling - gbc.gridx = 0; - gbc.gridy = row; - gbc.gridwidth = 1; - gbc.anchor = GridBagConstraints.WEST; - JButton submit = new JButton("Submit Data"); - submit.setPreferredSize(new java.awt.Dimension(0, 32)); - submit.addActionListener((ActionEvent e) -> save()); - submit.setMnemonic(KeyEvent.VK_S); - submit.setToolTipText("Save BrailleSense scores (Alt+S)"); - submit.getAccessibleContext().setAccessibleName("Submit BrailleSense Data"); - submit.setName("braillesense_submit"); - dataEntryPanel.add(submit, gbc); - - gbc.gridx = 1; - gbc.gridwidth = 1; - gbc.anchor = GridBagConstraints.WEST; - JButton openLatest = new JButton("Open Latest Plot"); - openLatest.setPreferredSize(new java.awt.Dimension(0, 32)); - openLatest.addActionListener((ActionEvent e) -> { - java.nio.file.Path p = com.studentgui.apphelpers.Helpers.latestPlotPath(this.studentNameParam, "BrailleSense"); - if (p == null) { - com.studentgui.apphelpers.UiNotifier.show("No BrailleSense plot found for student"); - } else { - try { - java.awt.Desktop.getDesktop().open(p.toFile()); - } catch (java.io.IOException | UnsupportedOperationException | SecurityException ex) { - com.studentgui.apphelpers.UiNotifier.show("Unable to open plot: " + p.getFileName().toString()); - } - } - }); - dataEntryPanel.add(openLatest, gbc); - - // Filler to consume remaining horizontal space - gbc.gridx = 2; - gbc.gridwidth = GridBagConstraints.REMAINDER; - gbc.anchor = GridBagConstraints.WEST; - dataEntryPanel.add(new JPanel(), gbc); - - dataEntryScrollPane.getAccessibleContext().setAccessibleName("BrailleSense data entry scroll pane"); - add(dataEntryScrollPane, BorderLayout.CENTER); - add(graph, BorderLayout.SOUTH); - SwingUtilities.invokeLater(() -> { - dataEntryPanel.setPreferredSize(dataEntryPanel.getPreferredSize()); - revalidate(); - }); - SwingUtilities.invokeLater(() -> { - for (var e : inputs.values()) { - LOG.debug("BrailleSense field {} labelWidth={} spinnerX={} gap={}", e.getLabel(), e.getLabelWrapWidth(), e.getSpinnerX(), e.getActualGap()); - } - }); - com.studentgui.apphelpers.Helpers.createFolderHierarchy(); - initParts(); - } - - /** - * Ensure the database contains the progress-type and assessment part rows - * for BrailleSense. Safe to call repeatedly. - */ - private void initParts() { - try { - int ptId = com.studentgui.apphelpers.Database.getOrCreateProgressType("BrailleSense"); - String[] codes = inputs.keySet().toArray(String[]::new); - com.studentgui.apphelpers.Database.ensureAssessmentParts(ptId, codes); - } catch (SQLException ex) { - LOG.error("Error ensuring braillesense parts", ex); - } - } - - /** - * Persist the current inputs as a new progress session for the selected - * student. Non-integer input is treated as zero. - */ - private void save() { - try { - int studentId = com.studentgui.apphelpers.Database.getOrCreateStudent(studentNameParam); - int ptId = com.studentgui.apphelpers.Database.getOrCreateProgressType("BrailleSense"); - int sessionId = com.studentgui.apphelpers.Database.createProgressSession(studentId, ptId, dateParam); - String[] codes = inputs.keySet().toArray(String[]::new); - int[] scores = new int[codes.length]; - for (int i = 0; i < codes.length; i++) { - scores[i] = inputs.get(codes[i]).getValue(); - } - com.studentgui.apphelpers.Database.insertAssessmentResults(sessionId, ptId, codes, scores); - LOG.info("BrailleSense data saved for {}", studentNameParam); - com.studentgui.apphelpers.UiNotifier.show("BrailleSense data saved."); - com.studentgui.apphelpers.dto.AssessmentPayload payload = new com.studentgui.apphelpers.dto.AssessmentPayload(sessionId, codes, scores); - java.nio.file.Path jsonOut = com.studentgui.apphelpers.SessionJsonWriter.writeSessionJson(this.studentNameParam, "BrailleSense", payload, sessionId); - if (jsonOut == null) { - LOG.warn("Unable to save BrailleSense session JSON for sessionId={}", sessionId); - } - try { - java.nio.file.Path plotsOut = com.studentgui.apphelpers.Helpers.studentPlotsDir(this.studentNameParam); - java.nio.file.Path reportsOut = com.studentgui.apphelpers.Helpers.studentReportsDir(this.studentNameParam); - java.nio.file.Files.createDirectories(plotsOut); - java.nio.file.Files.createDirectories(reportsOut); - java.time.format.DateTimeFormatter df = java.time.format.DateTimeFormatter.ISO_DATE; - String dateStr = this.dateParam != null ? this.dateParam.format(df) : java.time.LocalDate.now().toString(); - String baseName = "BrailleSense-" + sessionId + "-" + dateStr; - - com.studentgui.apphelpers.Database.ResultsWithDates rwd = com.studentgui.apphelpers.Database.fetchLatestAssessmentResultsWithDates(this.studentNameParam, "BrailleSense", Integer.MAX_VALUE); - java.util.Map groups = null; - String[] labels = java.util.Arrays.stream(this.parts).map(x -> x[1]).toArray(String[]::new); - if (rwd != null && rwd.rows != null && !rwd.rows.isEmpty()) { - graph.updateWithGroupedDataByDate(rwd.dates, rwd.rows, codes, labels); - groups = graph.saveGroupedCharts(plotsOut, baseName, 1000, 240); - java.time.LocalDate headerDate = rwd.dates.get(rwd.dates.size() - 1); - dateStr = headerDate.format(df); - } else { - java.util.List> rowsList = new java.util.ArrayList<>(); - java.util.List latest = new java.util.ArrayList<>(); - for (int i = 0; i < codes.length; i++) { - latest.add(inputs.get(codes[i]).getValue()); - } - rowsList.add(latest); - graph.updateWithGroupedData(rowsList, codes); - groups = graph.saveGroupedCharts(plotsOut, baseName, 1000, 240); - } - - if (groups == null) { - groups = new java.util.LinkedHashMap<>(); - } - StringBuilder md = new StringBuilder(); - md.append("# ").append(this.studentNameParam == null ? "Unknown Student" : this.studentNameParam).append(" - ").append(dateStr).append("\n\n"); - for (java.util.Map.Entry e : groups.entrySet()) { - md.append("## ").append(e.getKey()).append("\n\n"); - md.append("![](./").append(e.getValue().getFileName().toString()).append(")\n\n"); - } - // adjust markdown image links to point to the plots folder relative to reports - java.lang.String mdText = md.toString().replace("![](./", "![](../plots/"); - java.nio.file.Path mdFile = reportsOut.resolve(baseName + ".md"); - java.nio.file.Files.writeString(mdFile, mdText, java.nio.charset.StandardCharsets.UTF_8); - - try { - String[] palette = JLineGraph.PALETTE_HEX; - java.util.LinkedHashMap> groupsIdx = new java.util.LinkedHashMap<>(); - for (int i = 0; i < codes.length; i++) { - String code = codes[i]; - String grp = code != null && code.contains("_") ? code.split("_")[0] : code; - groupsIdx.computeIfAbsent(grp, k -> new java.util.ArrayList<>()).add(i); - } - StringBuilder html = new StringBuilder(); - html.append(""); - html.append(this.studentNameParam == null ? "Student Report" : this.studentNameParam).append(" - ").append(dateStr).append(""); - html.append(""); - html.append(""); - html.append("

").append(this.studentNameParam == null ? "Unknown Student" : this.studentNameParam).append(" - ").append(dateStr).append("

"); - for (java.util.Map.Entry e2 : groups.entrySet()) { - String grp = e2.getKey(); - String imgName = e2.getValue().getFileName().toString(); - html.append("

").append(grp).append("

"); - html.append("
\"").append(grp).append("\"
"); - java.util.List idxs = groupsIdx.getOrDefault(grp, new java.util.ArrayList<>()); - html.append("
"); - for (int s = 0; s < idxs.size(); s++) { - int idx = idxs.get(s); - String code = codes[idx]; - String human = this.parts[idx][1]; - String seriesName = code + " - " + human; - String color = palette[s % palette.length]; - html.append("
"); - html.append(""); - html.append("
"); - html.append(seriesName); - html.append("
"); - } - html.append("
"); - } - html.append(""); - java.nio.file.Path htmlFile = reportsOut.resolve(baseName + ".html"); - java.nio.file.Files.writeString(htmlFile, html.toString(), java.nio.charset.StandardCharsets.UTF_8); - LOG.info("Wrote BrailleSense HTML session report {}", htmlFile); - } catch (java.io.IOException ioex) { - LOG.warn("Unable to write BrailleSense HTML report: {}", ioex.toString()); - } - } catch (java.io.IOException ioe) { - LOG.warn("Unable to save BrailleSense per-phase plots or markdown report: {}", ioe.toString()); - } - } catch (SQLException ex) { - LOG.error("Error saving braillesense data", ex); - } - } - - // plotting is handled via submit/save which updates the shared graph and saves a static PNG -} +package com.studentgui.apppages; + +import java.awt.BorderLayout; +import java.awt.Font; +import java.awt.GridBagConstraints; +import java.awt.GridBagLayout; +import java.awt.Insets; +import java.awt.event.ActionEvent; +import java.awt.event.KeyEvent; +import java.sql.SQLException; +import java.time.LocalDate; +import java.util.LinkedHashMap; +import java.util.Map; + +import javax.swing.JButton; +import javax.swing.JLabel; +import javax.swing.JPanel; +import javax.swing.JScrollPane; +import javax.swing.SwingUtilities; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import com.studentgui.uicomp.PhaseScoreField; + +/** + * HIMS BrailleSense productivity device proficiency assessment page. + * + *

Evaluates student competency with the HIMS BrailleSense family of refreshable braille + * notetakers (BrailleSense Polaris, BrailleSense 6, etc.) across 52 skills organized into + * 12 functional domains. The BrailleSense assessment structure mirrors {@link BrailleNote} + * to allow cross-device skill comparison.

+ * + *

Device Family Context: The BrailleSense is a portable braille notetaker with + * refreshable braille display, perkins-style keyboard, and integrated productivity software. + * It runs proprietary HIMS firmware and includes word processing, email, web browsing, + * media playback, and educational applications.

+ * + *

Assessment Phases (12 domains, 52 skills):

+ *
    + *
  • Phase 1: Device fundamentals (layout, setup, navigation, file management, core apps)
  • + *
  • Phase 2: Productivity suite (calendar, email, web, calculator, word processor)
  • + *
  • Phase 3: Advanced apps (presentations, code editor, third-party integration, braille I/O)
  • + *
  • Phase 4: Cloud integration and advanced file management
  • + *
  • Phase 5: Collaboration, export/import, printing, backup workflows
  • + *
  • Phase 6: App installation, updates, troubleshooting
  • + *
  • Phase 7: Automation (custom shortcuts, macros, scripting)
  • + *
  • Phase 8: Peripheral connectivity (Bluetooth, USB, displays, audio/video)
  • + *
  • Phase 9: Security, user accounts, parental controls, network settings
  • + *
  • Phase 10: Speech customization (TTS settings, voice profiles, languages)
  • + *
  • Phase 11: Device maintenance (firmware, diagnostics, logs, support, warranty)
  • + *
  • Phase 12: Community resources (online help, forums, feedback channels)
  • + *
+ * + *

Data Management and Report Generation:

+ *
    + *
  • Scores captured via {@link PhaseScoreField} components (integer 0–4 typical)
  • + *
  • Persisted to normalized schema via {@link com.studentgui.apphelpers.Database#insertAssessmentResults}
  • + *
  • JSON export: {@code StudentDataFiles//Sessions/BrailleSense/BrailleSense--.json}
  • + *
  • Phase-grouped time-series plots: {@code plots/BrailleSense---P.png} (12 phase groups)
  • + *
  • Markdown and HTML reports with embedded plots and color-coded legends
  • + *
+ * + *

The shared {@link JLineGraph} visualizes recent session trends grouped by phase prefix. + * This page operates on static student/date parameters and does not implement listener interfaces.

+ * + * @see com.studentgui.apphelpers.Database + * @see JLineGraph + * @see PhaseScoreField + * @see BrailleNote + */ +public class BrailleSense extends JPanel { + private static final Logger LOG = LoggerFactory.getLogger(BrailleSense.class); + /** Map of assessment part codes to their input components. */ + private final Map inputs = new LinkedHashMap<>(); + /** Canonical assessment parts for BrailleSense. */ + private final String[][] parts; + /** Selected student display name (may be null). */ + private final String studentNameParam; + /** Date associated with the current session. */ + private final LocalDate dateParam; + /** Shared graph component used to visualize recent results. */ + private final JLineGraph graph; + + /** + * Create a BrailleSense page bound to the provided student and date. + * + * @param studentName selected student name (may be null until selection) + * @param date session date to associate with persisted progress rows + * @param graph shared graph component used to plot recent results + */ + public BrailleSense(String studentName, LocalDate date, JLineGraph graph) { + this.studentNameParam = (studentName == null || studentName.trim().isEmpty()) ? com.studentgui.apphelpers.Helpers.defaultStudent() : studentName; + this.dateParam = date; + this.graph = graph; + setLayout(new BorderLayout()); + + // create a data entry panel that mirrors BrailleNote's layout so alignment is identical + JPanel dataEntryPanel = new JPanel(new GridBagLayout()); + JPanel view = new JPanel(new BorderLayout()); + view.add(dataEntryPanel, BorderLayout.NORTH); + view.setBorder(javax.swing.BorderFactory.createEmptyBorder(20, 20, 20, 20)); + JScrollPane dataEntryScrollPane = new JScrollPane(view); + dataEntryScrollPane.setVerticalScrollBarPolicy(javax.swing.JScrollPane.VERTICAL_SCROLLBAR_AS_NEEDED); + dataEntryScrollPane.setHorizontalScrollBarPolicy(javax.swing.JScrollPane.HORIZONTAL_SCROLLBAR_NEVER); + dataEntryScrollPane.getAccessibleContext().setAccessibleName("BrailleSense data entry scroll pane"); + + GridBagConstraints gbc = new GridBagConstraints(); + gbc.insets = new Insets(2, 2, 2, 2); + gbc.fill = GridBagConstraints.HORIZONTAL; + gbc.weightx = 1.0; + gbc.weighty = 0.0; + + JLabel titleLabel = new JLabel("BrailleSense Skills"); + // Use an explicit font so theme changes don't alter the title appearance + titleLabel.setFont(new Font(Font.SANS_SERIF, Font.BOLD, 16)); + gbc.gridx = 0; + gbc.gridy = 0; + gbc.gridwidth = GridBagConstraints.REMAINDER; + dataEntryPanel.add(titleLabel, gbc); + + gbc.gridy = 1; + gbc.gridwidth = GridBagConstraints.REMAINDER; + gbc.ipady = 20; + dataEntryPanel.add(new JPanel(), gbc); + + this.parts = new String[][]{ + {"P1_1", "1.1 Physical Layout"}, {"P1_2", "1.2 Setup/Universal Commands"}, {"P1_3", "1.3 BNT+ Navigation"}, {"P1_4", "1.4 File Management"}, {"P1_5", "1.5 Word Processor"}, {"P1_6", "1.6 Email"}, {"P1_7", "1.7 Internet"}, {"P1_8", "1.8 Calculator"}, {"P1_9", "1.9 KeyMath"}, + {"P2_1", "2.1 Calendar"}, {"P2_2", "2.2 KeyBRF"}, {"P2_3", "2.3 KeyFiles"}, {"P2_4", "2.4 KeyMail"}, {"P2_5", "2.5 KeyWeb"}, {"P2_6", "2.6 KeyCalc"}, {"P2_7", "2.7 KeyWord"}, + {"P3_1", "3.1 KeySlides"}, {"P3_2", "3.2 KeyCode"}, {"P3_3", "3.3 Third Party Apps"}, {"P3_4", "3.4 Braille Input"}, {"P3_5", "3.5 Braille Output"}, {"P3_6", "3.6 Settings"}, {"P3_7", "3.7 Accessibility"}, + {"P4_1", "4.1 Advanced File Management"}, {"P4_2", "4.2 Cloud Integration"}, {"P4_3", "4.3 Device Maintenance"}, + {"P5_1", "5.1 Collaboration"}, {"P5_2", "5.2 Export/Import"}, {"P5_3", "5.3 Printing"}, {"P5_4", "5.4 Backup"}, + {"P6_1", "6.1 App Installation"}, {"P6_2", "6.2 App Updates"}, {"P6_3", "6.3 Troubleshooting"}, + {"P7_1", "7.1 Custom Shortcuts"}, {"P7_2", "7.2 Macros"}, {"P7_3", "7.3 Scripting"}, {"P7_4", "7.4 Automation"}, + {"P8_1", "8.1 Bluetooth Devices"}, {"P8_2", "8.2 USB Devices"}, {"P8_3", "8.3 External Displays"}, {"P8_4", "8.4 Audio Output"}, {"P8_5", "8.5 Video Output"}, + {"P9_1", "9.1 Security"}, {"P9_2", "9.2 User Accounts"}, {"P9_3", "9.3 Parental Controls"}, {"P9_4", "9.4 Network Settings"}, + {"P10_1", "10.1 Speech Settings"}, {"P10_2", "10.2 Voice Profiles"}, {"P10_3", "10.3 Language Support"}, + {"P11_1", "11.1 Firmware Updates"}, {"P11_2", "11.2 Diagnostics"}, {"P11_3", "11.3 Logs"}, {"P11_4", "11.4 Support"}, {"P11_5", "11.5 Warranty"}, + {"P12_1", "12.1 Community Resources"}, {"P12_2", "12.2 Online Help"}, {"P12_3", "12.3 User Forums"}, {"P12_4", "12.4 Feedback"} + }; + + // compute pixel width using font metrics so labels align precisely + String[] labels = java.util.Arrays.stream(this.parts).map(x -> x[1]).toArray(String[]::new); + int maxPx = com.studentgui.uicomp.PhaseScoreField.computeMaxLabelPixelWidth(titleLabel.getFont(), labels); + com.studentgui.uicomp.PhaseScoreField.setGlobalLabelWidth(Math.min(360, Math.max(200, maxPx + 50))); + int row = 1; + for (String[] def : this.parts) { + gbc.gridx = 0; + gbc.gridy = row; + gbc.gridwidth = 2; + PhaseScoreField tf = new PhaseScoreField(def[1], 0); + tf.setName("braillesense_" + def[0]); + tf.getAccessibleContext().setAccessibleName(def[1]); + tf.setToolTipText("Enter score for " + def[1]); + dataEntryPanel.add(tf, gbc); + inputs.put(def[0], tf); + row++; + } + + // Place Submit and Open Latest side-by-side to match IOS/ScreenReader styling + gbc.gridx = 0; + gbc.gridy = row; + gbc.gridwidth = 1; + gbc.anchor = GridBagConstraints.WEST; + JButton submit = new JButton("Submit Data"); + submit.setPreferredSize(new java.awt.Dimension(0, 32)); + submit.addActionListener((ActionEvent e) -> save()); + submit.setMnemonic(KeyEvent.VK_S); + submit.setToolTipText("Save BrailleSense scores (Alt+S)"); + submit.getAccessibleContext().setAccessibleName("Submit BrailleSense Data"); + submit.setName("braillesense_submit"); + dataEntryPanel.add(submit, gbc); + + gbc.gridx = 1; + gbc.gridwidth = 1; + gbc.anchor = GridBagConstraints.WEST; + JButton openLatest = new JButton("Open Latest Plot"); + openLatest.setPreferredSize(new java.awt.Dimension(0, 32)); + openLatest.addActionListener((ActionEvent e) -> { + java.nio.file.Path p = com.studentgui.apphelpers.Helpers.latestPlotPath(this.studentNameParam, "BrailleSense"); + if (p == null) { + com.studentgui.apphelpers.UiNotifier.show("No BrailleSense plot found for student"); + } else { + try { + java.awt.Desktop.getDesktop().open(p.toFile()); + } catch (java.io.IOException | UnsupportedOperationException | SecurityException ex) { + com.studentgui.apphelpers.UiNotifier.show("Unable to open plot: " + p.getFileName().toString()); + } + } + }); + dataEntryPanel.add(openLatest, gbc); + + // Filler to consume remaining horizontal space + gbc.gridx = 2; + gbc.gridwidth = GridBagConstraints.REMAINDER; + gbc.anchor = GridBagConstraints.WEST; + dataEntryPanel.add(new JPanel(), gbc); + + dataEntryScrollPane.getAccessibleContext().setAccessibleName("BrailleSense data entry scroll pane"); + add(dataEntryScrollPane, BorderLayout.CENTER); + add(graph, BorderLayout.SOUTH); + SwingUtilities.invokeLater(() -> { + dataEntryPanel.setPreferredSize(dataEntryPanel.getPreferredSize()); + revalidate(); + }); + SwingUtilities.invokeLater(() -> { + for (var e : inputs.values()) { + LOG.debug("BrailleSense field {} labelWidth={} spinnerX={} gap={}", e.getLabel(), e.getLabelWrapWidth(), e.getSpinnerX(), e.getActualGap()); + } + }); + com.studentgui.apphelpers.Helpers.createFolderHierarchy(); + initParts(); + } + + /** + * Ensure the database contains the progress-type and assessment part rows + * for BrailleSense. Safe to call repeatedly. + */ + private void initParts() { + try { + int ptId = com.studentgui.apphelpers.Database.getOrCreateProgressType("BrailleSense"); + String[] codes = inputs.keySet().toArray(String[]::new); + com.studentgui.apphelpers.Database.ensureAssessmentParts(ptId, codes); + } catch (SQLException ex) { + LOG.error("Error ensuring braillesense parts", ex); + } + } + + /** + * Persist the current inputs as a new progress session for the selected + * student. Non-integer input is treated as zero. + */ + private void save() { + try { + int studentId = com.studentgui.apphelpers.Database.getOrCreateStudent(studentNameParam); + int ptId = com.studentgui.apphelpers.Database.getOrCreateProgressType("BrailleSense"); + int sessionId = com.studentgui.apphelpers.Database.createProgressSession(studentId, ptId, dateParam); + String[] codes = inputs.keySet().toArray(String[]::new); + int[] scores = new int[codes.length]; + for (int i = 0; i < codes.length; i++) { + scores[i] = inputs.get(codes[i]).getValue(); + } + com.studentgui.apphelpers.Database.insertAssessmentResults(sessionId, ptId, codes, scores); + LOG.info("BrailleSense data saved for {}", studentNameParam); + com.studentgui.apphelpers.UiNotifier.show("BrailleSense data saved."); + com.studentgui.apphelpers.dto.AssessmentPayload payload = new com.studentgui.apphelpers.dto.AssessmentPayload(sessionId, codes, scores); + java.nio.file.Path jsonOut = com.studentgui.apphelpers.SessionJsonWriter.writeSessionJson(this.studentNameParam, "BrailleSense", payload, sessionId); + if (jsonOut == null) { + LOG.warn("Unable to save BrailleSense session JSON for sessionId={}", sessionId); + } + try { + java.nio.file.Path plotsOut = com.studentgui.apphelpers.Helpers.studentPlotsDir(this.studentNameParam); + java.nio.file.Path reportsOut = com.studentgui.apphelpers.Helpers.studentReportsDir(this.studentNameParam); + java.nio.file.Files.createDirectories(plotsOut); + java.nio.file.Files.createDirectories(reportsOut); + java.time.format.DateTimeFormatter df = java.time.format.DateTimeFormatter.ISO_DATE; + String dateStr = this.dateParam != null ? this.dateParam.format(df) : java.time.LocalDate.now().toString(); + String baseName = "BrailleSense-" + sessionId + "-" + dateStr; + + com.studentgui.apphelpers.Database.ResultsWithDates rwd = com.studentgui.apphelpers.Database.fetchLatestAssessmentResultsWithDates(this.studentNameParam, "BrailleSense", Integer.MAX_VALUE); + java.util.Map groups = null; + String[] labels = java.util.Arrays.stream(this.parts).map(x -> x[1]).toArray(String[]::new); + if (rwd != null && rwd.rows != null && !rwd.rows.isEmpty()) { + graph.updateWithGroupedDataByDate(rwd.dates, rwd.rows, codes, labels); + groups = graph.saveGroupedCharts(plotsOut, baseName, 1000, 240); + java.time.LocalDate headerDate = rwd.dates.get(rwd.dates.size() - 1); + dateStr = headerDate.format(df); + } else { + java.util.List> rowsList = new java.util.ArrayList<>(); + java.util.List latest = new java.util.ArrayList<>(); + for (int i = 0; i < codes.length; i++) { + latest.add(inputs.get(codes[i]).getValue()); + } + rowsList.add(latest); + graph.updateWithGroupedData(rowsList, codes); + groups = graph.saveGroupedCharts(plotsOut, baseName, 1000, 240); + } + + if (groups == null) { + groups = new java.util.LinkedHashMap<>(); + } + StringBuilder md = new StringBuilder(); + md.append("# ").append(this.studentNameParam == null ? "Unknown Student" : this.studentNameParam).append(" - ").append(dateStr).append("\n\n"); + for (java.util.Map.Entry e : groups.entrySet()) { + md.append("## ").append(e.getKey()).append("\n\n"); + md.append("![](./").append(e.getValue().getFileName().toString()).append(")\n\n"); + } + // adjust markdown image links to point to the plots folder relative to reports + java.lang.String mdText = md.toString().replace("![](./", "![](../plots/"); + java.nio.file.Path mdFile = reportsOut.resolve(baseName + ".md"); + java.nio.file.Files.writeString(mdFile, mdText, java.nio.charset.StandardCharsets.UTF_8); + + try { + String[] palette = JLineGraph.PALETTE_HEX; + java.util.LinkedHashMap> groupsIdx = new java.util.LinkedHashMap<>(); + for (int i = 0; i < codes.length; i++) { + String code = codes[i]; + String grp = code != null && code.contains("_") ? code.split("_")[0] : code; + groupsIdx.computeIfAbsent(grp, k -> new java.util.ArrayList<>()).add(i); + } + StringBuilder html = new StringBuilder(); + html.append(""); + html.append(this.studentNameParam == null ? "Student Report" : this.studentNameParam).append(" - ").append(dateStr).append(""); + html.append(""); + html.append(""); + html.append("

").append(this.studentNameParam == null ? "Unknown Student" : this.studentNameParam).append(" - ").append(dateStr).append("

"); + for (java.util.Map.Entry e2 : groups.entrySet()) { + String grp = e2.getKey(); + String imgName = e2.getValue().getFileName().toString(); + html.append("

").append(grp).append("

"); + html.append("
\"").append(grp).append("\"
"); + java.util.List idxs = groupsIdx.getOrDefault(grp, new java.util.ArrayList<>()); + html.append("
"); + for (int s = 0; s < idxs.size(); s++) { + int idx = idxs.get(s); + String code = codes[idx]; + String human = this.parts[idx][1]; + String seriesName = code + " - " + human; + String color = palette[s % palette.length]; + html.append("
"); + html.append(""); + html.append("
"); + html.append(seriesName); + html.append("
"); + } + html.append("
"); + } + html.append(""); + java.nio.file.Path htmlFile = reportsOut.resolve(baseName + ".html"); + java.nio.file.Files.writeString(htmlFile, html.toString(), java.nio.charset.StandardCharsets.UTF_8); + LOG.info("Wrote BrailleSense HTML session report {}", htmlFile); + } catch (java.io.IOException ioex) { + LOG.warn("Unable to write BrailleSense HTML report: {}", ioex.toString()); + } + } catch (java.io.IOException ioe) { + LOG.warn("Unable to save BrailleSense per-phase plots or markdown report: {}", ioe.toString()); + } + } catch (SQLException ex) { + LOG.error("Error saving braillesense data", ex); + } + } + + // plotting is handled via submit/save which updates the shared graph and saves a static PNG +} diff --git a/src/main/java/com/studentgui/apppages/CVI.java b/src/main/java/com/studentgui/apppages/CVI.java index ffd9832..9462d64 100644 --- a/src/main/java/com/studentgui/apppages/CVI.java +++ b/src/main/java/com/studentgui/apppages/CVI.java @@ -1,260 +1,302 @@ -package com.studentgui.apppages; - -import java.awt.BorderLayout; -import java.awt.Font; -import java.awt.GridBagConstraints; -import java.awt.GridBagLayout; -import java.awt.Insets; -import java.awt.event.ActionEvent; -import java.awt.event.KeyEvent; -import java.sql.SQLException; -import java.time.LocalDate; -import java.util.LinkedHashMap; -import java.util.Map; - -import javax.swing.JButton; -import javax.swing.JLabel; -import javax.swing.JPanel; -import javax.swing.JScrollPane; -import javax.swing.SwingUtilities; - -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; - -import com.studentgui.uicomp.PhaseScoreField; - -/** - * Cortical Visual Impairment (CVI) progression page. - *

- * Presents a collection of named inputs for CVI-related observation scores - * and supports saving and plotting recent sessions via the shared - * {@link JLineGraph} component. - *

- */ -public class CVI extends JPanel { - private static final Logger LOG = LoggerFactory.getLogger(CVI.class); - /** Mapping of assessment part codes to their input components. */ - private final Map inputs = new LinkedHashMap<>(); - - /** Selected student display name (may be null) used when saving or plotting. */ - private final String studentNameParam; - - /** Session date to associate with saved CVI progress entries. */ - private final LocalDate dateParam; - - /** Shared graph component used to visualize recent CVI results. */ - private final JLineGraph graph; - - /** - * Construct the CVI page bound to the selected student and session date. - * - * @param studentName selected student name (may be null) - * @param date session date to use when creating progress sessions - * @param graph shared graph used to visualize recent results - */ - public CVI(String studentName, LocalDate date, JLineGraph graph) { - this.studentNameParam = (studentName == null || studentName.trim().isEmpty()) ? com.studentgui.apphelpers.Helpers.defaultStudent() : studentName; - this.dateParam = date; - this.graph = graph; - setLayout(new BorderLayout()); - JPanel panel = new JPanel(new GridBagLayout()); - JPanel view = new JPanel(new BorderLayout()); - view.add(panel, BorderLayout.NORTH); - view.setBorder(javax.swing.BorderFactory.createEmptyBorder(20,20,20,20)); - JScrollPane scroll = new JScrollPane(view); - GridBagConstraints gbc = new GridBagConstraints(); gbc.insets=new Insets(2,2,2,2); gbc.fill = GridBagConstraints.HORIZONTAL; gbc.anchor = GridBagConstraints.NORTHWEST; - - JLabel title = new JLabel("CVI Progression"); - title.setFont(title.getFont().deriveFont(Font.BOLD,16)); - title.getAccessibleContext().setAccessibleName("CVI Progression Title"); - title.setHorizontalAlignment(JLabel.LEFT); - gbc.gridx=0; gbc.gridy=0; gbc.gridwidth=2; panel.add(title, gbc); - - String[][] parts = new String[][]{{"P1_1","Color Preference"},{"P1_2","Need for Movement"},{"P1_3","Latency"},{"P1_4","Field Preference"},{"P1_5","Visual Complexity"},{"P1_6","Nonpurposeful Gaze"},{"P2_1","Distance Viewing"},{"P2_2","Atypical Reflexes"},{"P2_3","Visual Novelty"},{"P2_4","Visual Reach"}}; - String[] labels = java.util.Arrays.stream(parts).map(x->x[1]).toArray(String[]::new); - int maxPx = com.studentgui.uicomp.PhaseScoreField.computeMaxLabelPixelWidth(title.getFont(), labels); - com.studentgui.uicomp.PhaseScoreField.setGlobalLabelWidth(Math.min(320, Math.max(140, maxPx + 50))); - int row = 1; - for (String[] pdef: parts) { - gbc.gridx = 0; gbc.gridy = row; gbc.gridwidth = 2; - PhaseScoreField tf = new PhaseScoreField(pdef[1], 0); - tf.setToolTipText("Enter whole number score for " + pdef[1]); - tf.getAccessibleContext().setAccessibleName(pdef[1]); - tf.setName("cvi_" + pdef[0]); - panel.add(tf, gbc); - inputs.put(pdef[0], tf); - row++; - } - - // Two side-by-side buttons: Submit Data (save + save PNG) and Open Latest Plot - gbc.gridx = 0; gbc.gridy = row; gbc.gridwidth = 1; gbc.anchor = GridBagConstraints.WEST; - JButton submit = new JButton("Submit Data"); - submit.setPreferredSize(new java.awt.Dimension(0, 32)); - submit.addActionListener((ActionEvent e) -> save()); - submit.setToolTipText("Save CVI assessment for selected student (Alt+S)"); - submit.setMnemonic(KeyEvent.VK_S); - submit.getAccessibleContext().setAccessibleName("Submit CVI Data"); - submit.setName("cvi_submit"); - panel.add(submit, gbc); - - gbc.gridx = 1; gbc.gridwidth = 1; gbc.anchor = GridBagConstraints.WEST; - JButton openLatest = new JButton("Open Latest Plot"); - openLatest.setPreferredSize(new java.awt.Dimension(0, 32)); - openLatest.addActionListener((ActionEvent e) -> { - java.nio.file.Path plotPath = com.studentgui.apphelpers.Helpers.latestPlotPath(this.studentNameParam, "CVI"); - if (plotPath == null) com.studentgui.apphelpers.UiNotifier.show("No CVI plot found for student"); - else { - // Do not auto-open CVI plot on startup; only save it. Opening is handled - // by explicit user actions (Open Latest Plot). - } - }); - panel.add(openLatest, gbc); - gbc.gridwidth = 1; - - add(scroll, BorderLayout.CENTER); add(graph, BorderLayout.SOUTH); - SwingUtilities.invokeLater(()->{ panel.setPreferredSize(panel.getPreferredSize()); revalidate(); }); - com.studentgui.apphelpers.Helpers.createFolderHierarchy(); - initParts(); - } - - /** - * Ensure the CVI progress-type and part rows exist in the database. - */ - private void initParts() { - try { - int ptId = com.studentgui.apphelpers.Database.getOrCreateProgressType("CVI"); - java.util.Set keys = inputs.keySet(); - String[] codes = new String[keys.size()]; - int kidx = 0; - for (String k : keys) { - codes[kidx++] = k; - } - com.studentgui.apphelpers.Database.ensureAssessmentParts(ptId, codes); - } catch (SQLException ex) { - LOG.error("Error ensuring CVI parts", ex); - } - } - - /** - * Validate inputs and persist them as a new CVI progress session for the - * selected student. - */ - private void save() { - if (studentNameParam == null || studentNameParam.trim().isEmpty()) { - com.studentgui.apphelpers.UiNotifier.show("Please select a student before saving CVI data."); - return; - } - - try { - int studentId = com.studentgui.apphelpers.Database.getOrCreateStudent(studentNameParam); - int ptId = com.studentgui.apphelpers.Database.getOrCreateProgressType("CVI"); - int sessionId = com.studentgui.apphelpers.Database.createProgressSession(studentId, ptId, dateParam); - java.util.Set keys = inputs.keySet(); - String[] codes = new String[keys.size()]; - int kidx = 0; - for (String k : keys) codes[kidx++] = k; - int[] scores = new int[codes.length]; - for (int i = 0; i < codes.length; i++) { - scores[i] = inputs.get(codes[i]).getValue(); - } - com.studentgui.apphelpers.Database.insertAssessmentResults(sessionId, ptId, codes, scores); - LOG.info("CVI data saved for {}", studentNameParam); - com.studentgui.apphelpers.UiNotifier.show("CVI data saved."); - com.studentgui.apphelpers.dto.AssessmentPayload payload = new com.studentgui.apphelpers.dto.AssessmentPayload(sessionId, codes, scores); - java.nio.file.Path jsonOut = com.studentgui.apphelpers.SessionJsonWriter.writeSessionJson(this.studentNameParam, "CVI", payload, sessionId); - if (jsonOut == null) LOG.warn("Unable to save CVI session JSON for sessionId={}", sessionId); - try { - java.nio.file.Path plotsOut = com.studentgui.apphelpers.Helpers.studentPlotsDir(this.studentNameParam); - java.nio.file.Path reportsOut = com.studentgui.apphelpers.Helpers.studentReportsDir(this.studentNameParam); - java.nio.file.Files.createDirectories(plotsOut); - java.nio.file.Files.createDirectories(reportsOut); - java.time.format.DateTimeFormatter df = java.time.format.DateTimeFormatter.ISO_DATE; - String dateStr = this.dateParam != null ? this.dateParam.format(df) : java.time.LocalDate.now().toString(); - String baseName = "CVI-" + sessionId + "-" + dateStr; - - com.studentgui.apphelpers.Database.ResultsWithDates rwd = com.studentgui.apphelpers.Database.fetchLatestAssessmentResultsWithDates(this.studentNameParam, "CVI", Integer.MAX_VALUE); - java.util.Map groups = null; - String[] labels = new String[codes.length]; - for (int i = 0; i < codes.length; i++) labels[i] = inputs.get(codes[i]).getLabel(); - if (rwd != null && rwd.rows != null && !rwd.rows.isEmpty()) { - graph.updateWithGroupedDataByDate(rwd.dates, rwd.rows, codes, labels); - groups = graph.saveGroupedCharts(plotsOut, baseName, 1000, 240); - java.time.LocalDate headerDate = rwd.dates.get(rwd.dates.size() - 1); - dateStr = headerDate.format(df); - } else { - java.util.List> rowsList = new java.util.ArrayList<>(); - java.util.List latest = new java.util.ArrayList<>(); - for (String c : codes) { - latest.add(inputs.get(c).getValue()); - } - rowsList.add(latest); - graph.updateWithGroupedData(rowsList, codes); - groups = graph.saveGroupedCharts(plotsOut, baseName, 1000, 240); - } - - if (groups == null) groups = new java.util.LinkedHashMap<>(); - StringBuilder md = new StringBuilder(); - md.append("# ").append(this.studentNameParam == null ? "Unknown Student" : this.studentNameParam).append(" - ").append(dateStr).append("\n\n"); - for (java.util.Map.Entry e : groups.entrySet()) { - md.append("## ").append(e.getKey()).append("\n\n"); - md.append("![](../plots/").append(e.getValue().getFileName().toString()).append(")\n\n"); - } - java.nio.file.Path mdFile = reportsOut.resolve(baseName + ".md"); - java.nio.file.Files.writeString(mdFile, md.toString(), java.nio.charset.StandardCharsets.UTF_8); - - try { - String[] palette = JLineGraph.PALETTE_HEX; - java.util.LinkedHashMap> groupsIdx = new java.util.LinkedHashMap<>(); - for (int i = 0; i < codes.length; i++) { - String code = codes[i]; - String grp = code != null && code.contains("_") ? code.split("_")[0] : code; - groupsIdx.computeIfAbsent(grp, k -> new java.util.ArrayList<>()).add(i); - } - StringBuilder html = new StringBuilder(); - html.append(""); - html.append(this.studentNameParam == null ? "Student Report" : this.studentNameParam).append(" - ").append(dateStr).append(""); - html.append(""); - html.append(""); - html.append("

").append(this.studentNameParam == null ? "Unknown Student" : this.studentNameParam).append(" - ").append(dateStr).append("

"); - for (java.util.Map.Entry e2 : groups.entrySet()) { - String grp = e2.getKey(); - String imgName = e2.getValue().getFileName().toString(); - html.append("

").append(grp).append("

"); - html.append("
\"").append(grp).append("\"
"); - java.util.List idxs = groupsIdx.getOrDefault(grp, new java.util.ArrayList<>()); - html.append("
"); - for (int s = 0; s < idxs.size(); s++) { - int idx = idxs.get(s); - String code = codes[idx]; - String human = labels[idx]; - String seriesName = code + " - " + human; - String color = palette[s % palette.length]; - html.append("
"); - html.append(""); - html.append("
"); - html.append(seriesName); - html.append("
"); - } - html.append("
"); - } - html.append(""); - java.nio.file.Path htmlFile = reportsOut.resolve(baseName + ".html"); - java.nio.file.Files.writeString(htmlFile, html.toString(), java.nio.charset.StandardCharsets.UTF_8); - LOG.info("Wrote CVI HTML session report {}", htmlFile); - } catch (java.io.IOException ioex) { - LOG.warn("Unable to write CVI HTML report: {}", ioex.toString()); - } - } catch (java.io.IOException ioe) { - LOG.warn("Unable to save CVI per-phase plots or markdown report: {}", ioe.toString()); - } - } catch (SQLException ex) { - LOG.error("Error saving CVI data", ex); - com.studentgui.apphelpers.UiNotifier.show("Database error saving CVI data: " + ex.getMessage()); - } - } - - // Plotting is handled as part of save(): the submit action updates the shared - // graph and writes a static PNG into the student's plots folder. -} +package com.studentgui.apppages; + +import java.awt.BorderLayout; +import java.awt.Font; +import java.awt.GridBagConstraints; +import java.awt.GridBagLayout; +import java.awt.Insets; +import java.awt.event.ActionEvent; +import java.awt.event.KeyEvent; +import java.sql.SQLException; +import java.time.LocalDate; +import java.util.LinkedHashMap; +import java.util.Map; + +import javax.swing.JButton; +import javax.swing.JLabel; +import javax.swing.JPanel; +import javax.swing.JScrollPane; +import javax.swing.SwingUtilities; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import com.studentgui.uicomp.PhaseScoreField; + +/** + * Cortical Visual Impairment (CVI) assessment page. + * + *

Provides a structured scoring interface for evaluating the 10 characteristic behaviors + * associated with Cortical Visual Impairment as defined in the Roman-Lanzi CVI Range assessment + * framework. Skills are organized into two functional clusters:

+ * + *
    + *
  • Phase 1 (P1_1–P1_6): Primary CVI Characteristics + *
      + *
    • Color Preference: Preference for high-saturation colors (red, yellow)
    • + *
    • Need for Movement: Improved visual attention with motion
    • + *
    • Latency: Delayed visual response times
    • + *
    • Field Preference: Asymmetric visual field usage patterns
    • + *
    • Visual Complexity: Difficulty with cluttered/busy visual environments
    • + *
    • Nonpurposeful Gaze: Reduced sustained visual fixation
    • + *
    + *
  • + *
  • Phase 2 (P2_1–P2_4): Secondary/Environmental Characteristics + *
      + *
    • Distance Viewing: Reduced effectiveness at distance
    • + *
    • Atypical Reflexes: Blink-to-threat, light reflex variations
    • + *
    • Visual Novelty: Preference for familiar objects/environments
    • + *
    • Visual Reach: Difficulty localizing and reaching toward objects
    • + *
    + *
  • + *
+ * + *

Scoring and Interpretation: Each characteristic is typically scored on a 0–10 scale + * representing frequency/severity of the behavior. Higher scores may indicate greater impact + * depending on the specific assessment protocol in use. Consult the Roman-Lanzi CVI Range manual + * for standardized scoring guidelines.

+ * + *

Data Management:

+ *
    + *
  • Scores captured via {@link PhaseScoreField} components with integer validation
  • + *
  • Submit button persists to database via {@link com.studentgui.apphelpers.Database#insertAssessmentResults}
  • + *
  • Session JSON exported to {@code StudentDataFiles//Sessions/CVI/CVI--.json}
  • + *
  • Time-series plots generated per phase group and saved to {@code plots/} directory
  • + *
  • Markdown and HTML reports generated with embedded plots and color-coded legends
  • + *
+ * + *

The shared {@link JLineGraph} component visualizes trends across multiple sessions, + * grouped by phase to separate primary and secondary characteristics. This page does not + * implement listener interfaces as it operates on static student/date parameters.

+ * + * @see com.studentgui.apphelpers.Database + * @see JLineGraph + * @see PhaseScoreField + */ +public class CVI extends JPanel { + private static final Logger LOG = LoggerFactory.getLogger(CVI.class); + /** Mapping of assessment part codes to their input components. */ + private final Map inputs = new LinkedHashMap<>(); + + /** Selected student display name (may be null) used when saving or plotting. */ + private final String studentNameParam; + + /** Session date to associate with saved CVI progress entries. */ + private final LocalDate dateParam; + + /** Shared graph component used to visualize recent CVI results. */ + private final JLineGraph graph; + + /** + * Construct the CVI page bound to the selected student and session date. + * + * @param studentName selected student name (may be null) + * @param date session date to use when creating progress sessions + * @param graph shared graph used to visualize recent results + */ + public CVI(String studentName, LocalDate date, JLineGraph graph) { + this.studentNameParam = (studentName == null || studentName.trim().isEmpty()) ? com.studentgui.apphelpers.Helpers.defaultStudent() : studentName; + this.dateParam = date; + this.graph = graph; + setLayout(new BorderLayout()); + JPanel panel = new JPanel(new GridBagLayout()); + JPanel view = new JPanel(new BorderLayout()); + view.add(panel, BorderLayout.NORTH); + view.setBorder(javax.swing.BorderFactory.createEmptyBorder(20,20,20,20)); + JScrollPane scroll = new JScrollPane(view); + GridBagConstraints gbc = new GridBagConstraints(); gbc.insets=new Insets(2,2,2,2); gbc.fill = GridBagConstraints.HORIZONTAL; gbc.anchor = GridBagConstraints.NORTHWEST; + + JLabel title = new JLabel("CVI Progression"); + title.setFont(title.getFont().deriveFont(Font.BOLD,16)); + title.getAccessibleContext().setAccessibleName("CVI Progression Title"); + title.setHorizontalAlignment(JLabel.LEFT); + gbc.gridx=0; gbc.gridy=0; gbc.gridwidth=2; panel.add(title, gbc); + + String[][] parts = new String[][]{{"P1_1","Color Preference"},{"P1_2","Need for Movement"},{"P1_3","Latency"},{"P1_4","Field Preference"},{"P1_5","Visual Complexity"},{"P1_6","Nonpurposeful Gaze"},{"P2_1","Distance Viewing"},{"P2_2","Atypical Reflexes"},{"P2_3","Visual Novelty"},{"P2_4","Visual Reach"}}; + String[] labels = java.util.Arrays.stream(parts).map(x->x[1]).toArray(String[]::new); + int maxPx = com.studentgui.uicomp.PhaseScoreField.computeMaxLabelPixelWidth(title.getFont(), labels); + com.studentgui.uicomp.PhaseScoreField.setGlobalLabelWidth(Math.min(320, Math.max(140, maxPx + 50))); + int row = 1; + for (String[] pdef: parts) { + gbc.gridx = 0; gbc.gridy = row; gbc.gridwidth = 2; + PhaseScoreField tf = new PhaseScoreField(pdef[1], 0); + tf.setToolTipText("Enter whole number score for " + pdef[1]); + tf.getAccessibleContext().setAccessibleName(pdef[1]); + tf.setName("cvi_" + pdef[0]); + panel.add(tf, gbc); + inputs.put(pdef[0], tf); + row++; + } + + // Two side-by-side buttons: Submit Data (save + save PNG) and Open Latest Plot + gbc.gridx = 0; gbc.gridy = row; gbc.gridwidth = 1; gbc.anchor = GridBagConstraints.WEST; + JButton submit = new JButton("Submit Data"); + submit.setPreferredSize(new java.awt.Dimension(0, 32)); + submit.addActionListener((ActionEvent e) -> save()); + submit.setToolTipText("Save CVI assessment for selected student (Alt+S)"); + submit.setMnemonic(KeyEvent.VK_S); + submit.getAccessibleContext().setAccessibleName("Submit CVI Data"); + submit.setName("cvi_submit"); + panel.add(submit, gbc); + + gbc.gridx = 1; gbc.gridwidth = 1; gbc.anchor = GridBagConstraints.WEST; + JButton openLatest = new JButton("Open Latest Plot"); + openLatest.setPreferredSize(new java.awt.Dimension(0, 32)); + openLatest.addActionListener((ActionEvent e) -> { + java.nio.file.Path plotPath = com.studentgui.apphelpers.Helpers.latestPlotPath(this.studentNameParam, "CVI"); + if (plotPath == null) com.studentgui.apphelpers.UiNotifier.show("No CVI plot found for student"); + else { + // Do not auto-open CVI plot on startup; only save it. Opening is handled + // by explicit user actions (Open Latest Plot). + } + }); + panel.add(openLatest, gbc); + gbc.gridwidth = 1; + + add(scroll, BorderLayout.CENTER); add(graph, BorderLayout.SOUTH); + SwingUtilities.invokeLater(()->{ panel.setPreferredSize(panel.getPreferredSize()); revalidate(); }); + com.studentgui.apphelpers.Helpers.createFolderHierarchy(); + initParts(); + } + + /** + * Ensure the CVI progress-type and part rows exist in the database. + */ + private void initParts() { + try { + int ptId = com.studentgui.apphelpers.Database.getOrCreateProgressType("CVI"); + java.util.Set keys = inputs.keySet(); + String[] codes = new String[keys.size()]; + int kidx = 0; + for (String k : keys) { + codes[kidx++] = k; + } + com.studentgui.apphelpers.Database.ensureAssessmentParts(ptId, codes); + } catch (SQLException ex) { + LOG.error("Error ensuring CVI parts", ex); + } + } + + /** + * Validate inputs and persist them as a new CVI progress session for the + * selected student. + */ + private void save() { + if (studentNameParam == null || studentNameParam.trim().isEmpty()) { + com.studentgui.apphelpers.UiNotifier.show("Please select a student before saving CVI data."); + return; + } + + try { + int studentId = com.studentgui.apphelpers.Database.getOrCreateStudent(studentNameParam); + int ptId = com.studentgui.apphelpers.Database.getOrCreateProgressType("CVI"); + int sessionId = com.studentgui.apphelpers.Database.createProgressSession(studentId, ptId, dateParam); + java.util.Set keys = inputs.keySet(); + String[] codes = new String[keys.size()]; + int kidx = 0; + for (String k : keys) codes[kidx++] = k; + int[] scores = new int[codes.length]; + for (int i = 0; i < codes.length; i++) { + scores[i] = inputs.get(codes[i]).getValue(); + } + com.studentgui.apphelpers.Database.insertAssessmentResults(sessionId, ptId, codes, scores); + LOG.info("CVI data saved for {}", studentNameParam); + com.studentgui.apphelpers.UiNotifier.show("CVI data saved."); + com.studentgui.apphelpers.dto.AssessmentPayload payload = new com.studentgui.apphelpers.dto.AssessmentPayload(sessionId, codes, scores); + java.nio.file.Path jsonOut = com.studentgui.apphelpers.SessionJsonWriter.writeSessionJson(this.studentNameParam, "CVI", payload, sessionId); + if (jsonOut == null) LOG.warn("Unable to save CVI session JSON for sessionId={}", sessionId); + try { + java.nio.file.Path plotsOut = com.studentgui.apphelpers.Helpers.studentPlotsDir(this.studentNameParam); + java.nio.file.Path reportsOut = com.studentgui.apphelpers.Helpers.studentReportsDir(this.studentNameParam); + java.nio.file.Files.createDirectories(plotsOut); + java.nio.file.Files.createDirectories(reportsOut); + java.time.format.DateTimeFormatter df = java.time.format.DateTimeFormatter.ISO_DATE; + String dateStr = this.dateParam != null ? this.dateParam.format(df) : java.time.LocalDate.now().toString(); + String baseName = "CVI-" + sessionId + "-" + dateStr; + + com.studentgui.apphelpers.Database.ResultsWithDates rwd = com.studentgui.apphelpers.Database.fetchLatestAssessmentResultsWithDates(this.studentNameParam, "CVI", Integer.MAX_VALUE); + java.util.Map groups = null; + String[] labels = new String[codes.length]; + for (int i = 0; i < codes.length; i++) labels[i] = inputs.get(codes[i]).getLabel(); + if (rwd != null && rwd.rows != null && !rwd.rows.isEmpty()) { + graph.updateWithGroupedDataByDate(rwd.dates, rwd.rows, codes, labels); + groups = graph.saveGroupedCharts(plotsOut, baseName, 1000, 240); + java.time.LocalDate headerDate = rwd.dates.get(rwd.dates.size() - 1); + dateStr = headerDate.format(df); + } else { + java.util.List> rowsList = new java.util.ArrayList<>(); + java.util.List latest = new java.util.ArrayList<>(); + for (String c : codes) { + latest.add(inputs.get(c).getValue()); + } + rowsList.add(latest); + graph.updateWithGroupedData(rowsList, codes); + groups = graph.saveGroupedCharts(plotsOut, baseName, 1000, 240); + } + + if (groups == null) groups = new java.util.LinkedHashMap<>(); + StringBuilder md = new StringBuilder(); + md.append("# ").append(this.studentNameParam == null ? "Unknown Student" : this.studentNameParam).append(" - ").append(dateStr).append("\n\n"); + for (java.util.Map.Entry e : groups.entrySet()) { + md.append("## ").append(e.getKey()).append("\n\n"); + md.append("![](../plots/").append(e.getValue().getFileName().toString()).append(")\n\n"); + } + java.nio.file.Path mdFile = reportsOut.resolve(baseName + ".md"); + java.nio.file.Files.writeString(mdFile, md.toString(), java.nio.charset.StandardCharsets.UTF_8); + + try { + String[] palette = JLineGraph.PALETTE_HEX; + java.util.LinkedHashMap> groupsIdx = new java.util.LinkedHashMap<>(); + for (int i = 0; i < codes.length; i++) { + String code = codes[i]; + String grp = code != null && code.contains("_") ? code.split("_")[0] : code; + groupsIdx.computeIfAbsent(grp, k -> new java.util.ArrayList<>()).add(i); + } + StringBuilder html = new StringBuilder(); + html.append(""); + html.append(this.studentNameParam == null ? "Student Report" : this.studentNameParam).append(" - ").append(dateStr).append(""); + html.append(""); + html.append(""); + html.append("

").append(this.studentNameParam == null ? "Unknown Student" : this.studentNameParam).append(" - ").append(dateStr).append("

"); + for (java.util.Map.Entry e2 : groups.entrySet()) { + String grp = e2.getKey(); + String imgName = e2.getValue().getFileName().toString(); + html.append("

").append(grp).append("

"); + html.append("
\"").append(grp).append("\"
"); + java.util.List idxs = groupsIdx.getOrDefault(grp, new java.util.ArrayList<>()); + html.append("
"); + for (int s = 0; s < idxs.size(); s++) { + int idx = idxs.get(s); + String code = codes[idx]; + String human = labels[idx]; + String seriesName = code + " - " + human; + String color = palette[s % palette.length]; + html.append("
"); + html.append(""); + html.append("
"); + html.append(seriesName); + html.append("
"); + } + html.append("
"); + } + html.append(""); + java.nio.file.Path htmlFile = reportsOut.resolve(baseName + ".html"); + java.nio.file.Files.writeString(htmlFile, html.toString(), java.nio.charset.StandardCharsets.UTF_8); + LOG.info("Wrote CVI HTML session report {}", htmlFile); + } catch (java.io.IOException ioex) { + LOG.warn("Unable to write CVI HTML report: {}", ioex.toString()); + } + } catch (java.io.IOException ioe) { + LOG.warn("Unable to save CVI per-phase plots or markdown report: {}", ioe.toString()); + } + } catch (SQLException ex) { + LOG.error("Error saving CVI data", ex); + com.studentgui.apphelpers.UiNotifier.show("Database error saving CVI data: " + ex.getMessage()); + } + } + + // Plotting is handled as part of save(): the submit action updates the shared + // graph and writes a static PNG into the student's plots folder. +} diff --git a/src/main/java/com/studentgui/apppages/ContactLog.java b/src/main/java/com/studentgui/apppages/ContactLog.java index 1a7ea31..2ce65f0 100644 --- a/src/main/java/com/studentgui/apppages/ContactLog.java +++ b/src/main/java/com/studentgui/apppages/ContactLog.java @@ -1,205 +1,241 @@ -package com.studentgui.apppages; - -import java.awt.BorderLayout; -import java.awt.Font; -import java.awt.GridBagConstraints; -import java.awt.GridBagLayout; -import java.awt.Insets; -import java.awt.event.ActionEvent; -import java.awt.event.KeyEvent; -import java.sql.SQLException; -import java.time.LocalDate; - -import javax.swing.JButton; -import javax.swing.JComboBox; -import javax.swing.JLabel; -import javax.swing.JOptionPane; -import javax.swing.JPanel; -import javax.swing.JScrollPane; -import javax.swing.JTextArea; -import javax.swing.JTextField; -import javax.swing.SwingUtilities; - -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; - -/** - * Contact log page for storing freeform contact notes for a student. - *

- * Provides a multi-line text area for notes and persists them to the - * normalized database as session notes using the {@code Database} helper. - *

- */ -public class ContactLog extends JPanel { - private static final Logger LOG = LoggerFactory.getLogger(ContactLog.class); - /** Text area where the user enters contact notes for the selected student. */ - private final JTextArea notesArea; - // additional contact fields - /** Guardian or parent name associated with the student. */ - private final JTextField guardianField; - /** Phone number used for contact. */ - private final JTextField phoneField; - /** Email address used for contact. */ - private final JTextField emailField; - /** Method of contact (Phone/Email/In Person/Other). */ - private final JComboBox contactMethodCombo; - /** Short description of the response received during contact. */ - private final JTextField contactResponseField; - /** High-level/general contact notes (summary). */ - private final JTextField contactGeneralField; - /** Specific items or action points discussed during contact. */ - private final JTextField contactSpecificField; - - /** Selected student display name associated with this page instance (may be null). */ - private final String studentNameParam; - - /** Session date to associate with saved notes from this page. */ - private final LocalDate dateParam; - - /** - * Construct a ContactLog page for the provided student and date. - * - * @param studentName selected student display name (may be null) - * @param date session date to associate with saved notes - * @param graph shared graph component shown under the editor - */ - public ContactLog(String studentName, LocalDate date, JLineGraph graph) { - this.studentNameParam = (studentName == null || studentName.trim().isEmpty()) ? com.studentgui.apphelpers.Helpers.defaultStudent() : studentName; - this.dateParam = date; - setLayout(new BorderLayout()); - - JPanel p = new JPanel(new GridBagLayout()); - JPanel view = new JPanel(new BorderLayout()); - view.add(p, BorderLayout.NORTH); - view.setBorder(javax.swing.BorderFactory.createEmptyBorder(20,20,20,20)); - JScrollPane scroll = new JScrollPane(view); - scroll.getAccessibleContext().setAccessibleName("Contact Log data entry scroll pane"); - GridBagConstraints gbc = new GridBagConstraints(); gbc.insets=new Insets(2,2,2,2); gbc.fill = GridBagConstraints.HORIZONTAL; gbc.anchor = GridBagConstraints.NORTHWEST; - JLabel title = new JLabel("Contact Log"); title.setFont(title.getFont().deriveFont(Font.BOLD,16)); title.setHorizontalAlignment(JLabel.LEFT); gbc.gridx=0; gbc.gridy=0; gbc.gridwidth=2; p.add(title, gbc); - - // Structured contact fields (placed above notes) - int row = 1; - gbc.gridwidth = 1; - int globalLabel = com.studentgui.uicomp.PhaseScoreField.getGlobalLabelWidth(); - gbc.gridx = 0; gbc.gridy = row; JLabel guardianLabel = new JLabel("Guardian Name:"); guardianLabel.setPreferredSize(new java.awt.Dimension(globalLabel, guardianLabel.getPreferredSize().height)); p.add(guardianLabel, gbc); - guardianField = new JTextField(24); guardianField.setName("contactlog_guardian"); gbc.gridx = 1; p.add(guardianField, gbc); - row++; - gbc.gridx = 0; gbc.gridy = row; JLabel methodLabel = new JLabel("Contact Method:"); methodLabel.setPreferredSize(new java.awt.Dimension(globalLabel, methodLabel.getPreferredSize().height)); p.add(methodLabel, gbc); - contactMethodCombo = new JComboBox<>(new String[]{"Phone","Email","In Person","Other"}); contactMethodCombo.setName("contactlog_method"); gbc.gridx = 1; p.add(contactMethodCombo, gbc); - row++; - gbc.gridx = 0; gbc.gridy = row; JLabel phoneLabel = new JLabel("Phone Number:"); phoneLabel.setPreferredSize(new java.awt.Dimension(globalLabel, phoneLabel.getPreferredSize().height)); p.add(phoneLabel, gbc); - phoneField = new JTextField(18); phoneField.setName("contactlog_phone"); gbc.gridx = 1; p.add(phoneField, gbc); - row++; - gbc.gridx = 0; gbc.gridy = row; JLabel emailLabel = new JLabel("Email Address:"); emailLabel.setPreferredSize(new java.awt.Dimension(globalLabel, emailLabel.getPreferredSize().height)); p.add(emailLabel, gbc); - emailField = new JTextField(24); emailField.setName("contactlog_email"); gbc.gridx = 1; p.add(emailField, gbc); - row++; - gbc.gridx = 0; gbc.gridy = row; JLabel responseLabel = new JLabel("Contact Response:"); responseLabel.setPreferredSize(new java.awt.Dimension(globalLabel, responseLabel.getPreferredSize().height)); p.add(responseLabel, gbc); - contactResponseField = new JTextField(24); contactResponseField.setName("contactlog_response"); gbc.gridx = 1; p.add(contactResponseField, gbc); - row++; - gbc.gridx = 0; gbc.gridy = row; JLabel generalLabel = new JLabel("Contact General:"); generalLabel.setPreferredSize(new java.awt.Dimension(globalLabel, generalLabel.getPreferredSize().height)); p.add(generalLabel, gbc); - contactGeneralField = new JTextField(24); contactGeneralField.setName("contactlog_general"); gbc.gridx = 1; p.add(contactGeneralField, gbc); - row++; - gbc.gridx = 0; gbc.gridy = row; JLabel specificLabel = new JLabel("Contact Specific:"); specificLabel.setPreferredSize(new java.awt.Dimension(globalLabel, specificLabel.getPreferredSize().height)); p.add(specificLabel, gbc); - contactSpecificField = new JTextField(24); contactSpecificField.setName("contactlog_specific"); gbc.gridx = 1; p.add(contactSpecificField, gbc); - row++; - - // Notes label + text area with accessibility - gbc.gridx = 0; gbc.gridy = row; gbc.gridwidth = 2; JLabel notesLabel = new JLabel("Notes:"); notesLabel.setPreferredSize(new java.awt.Dimension(globalLabel, notesLabel.getPreferredSize().height)); p.add(notesLabel, gbc); - row++; - - gbc.gridx = 0; gbc.gridy = row; gbc.gridwidth = 2; notesArea = new JTextArea(8,40); notesArea.setLineWrap(true); notesArea.setWrapStyleWord(true); notesArea.setToolTipText("Enter contact notes for the student"); notesArea.getAccessibleContext().setAccessibleName("Contact notes"); JScrollPane notesScroll = new JScrollPane(notesArea); notesScroll.setHorizontalScrollBarPolicy(JScrollPane.HORIZONTAL_SCROLLBAR_NEVER); p.add(notesScroll, gbc); - notesArea.setName("contactlog_notes"); - notesLabel.setLabelFor(notesArea); - - row++; - gbc.gridx = 0; gbc.gridy = row; gbc.gridwidth = 1; JButton save = new JButton("Save Contact"); - save.addActionListener((ActionEvent e)-> saveContact()); - save.setToolTipText("Save contact notes to the database"); - save.setMnemonic(KeyEvent.VK_S); - save.getAccessibleContext().setAccessibleName("Save Contact Notes"); - save.setName("contactlog_save"); - p.add(save, gbc); - - gbc.gridx = 1; JButton load = new JButton("Load Last Contact"); - load.addActionListener((ActionEvent e) -> loadLastContact()); - load.setToolTipText("Load the most recent contact for the selected student"); - load.setName("contactlog_load"); - p.add(load, gbc); - - add(scroll, BorderLayout.CENTER); - - SwingUtilities.invokeLater(()->{ p.setPreferredSize(p.getPreferredSize()); revalidate(); }); - - com.studentgui.apphelpers.Helpers.createFolderHierarchy(); - } - - private void loadLastContact() { - try { - com.studentgui.apphelpers.dto.ContactPayload p = com.studentgui.apphelpers.Database.fetchLatestContactLog(this.studentNameParam); - if (p == null) { - com.studentgui.apphelpers.UiNotifier.show("No contact found for this student."); - return; - } - guardianField.setText(p.guardian != null ? p.guardian : ""); - String method = p.method != null ? p.method : ""; - if (method != null) { - contactMethodCombo.setSelectedItem(method); - } - phoneField.setText(p.phone != null ? p.phone : ""); - emailField.setText(p.email != null ? p.email : ""); - contactResponseField.setText(p.response != null ? p.response : ""); - contactGeneralField.setText(p.general != null ? p.general : ""); - contactSpecificField.setText(p.specific != null ? p.specific : ""); - notesArea.setText(p.notes != null ? p.notes : ""); - } catch (SQLException ex) { - LOG.error("Error loading last contact", ex); - JOptionPane.showMessageDialog(this, "Database error loading contact: " + ex.getMessage(), "Database error", JOptionPane.ERROR_MESSAGE); - } - } - - /** - * Persist the contact notes entered into the notes area as a session note - * for the selected student. Shows a confirmation dialog on success and - * error dialogs on failure. - */ - private void saveContact() { - try { - int studentId = com.studentgui.apphelpers.Database.getOrCreateStudent(this.studentNameParam); - int ptId = com.studentgui.apphelpers.Database.getOrCreateProgressType("ContactLog"); - int sessionId = com.studentgui.apphelpers.Database.createProgressSession(studentId, ptId, this.dateParam); - - String notes = notesArea.getText(); - String guardian = guardianField.getText(); - String method = (String) contactMethodCombo.getSelectedItem(); - String phone = phoneField.getText(); - String email = emailField.getText(); - String response = contactResponseField.getText(); - String general = contactGeneralField.getText(); - String specific = contactSpecificField.getText(); - - // Basic validation - if (method != null && method.equals("Email") && (email == null || !email.matches("^[^@\\s]+@[^@\\s]+\\.[^@\\s]+$"))) { - JOptionPane.showMessageDialog(this, "Please enter a valid email address.", "Validation", JOptionPane.WARNING_MESSAGE); - return; - } - if (method != null && method.equals("Phone") && (phone == null || !phone.matches("^[0-9+()\\-\s]{7,20}$"))) { - JOptionPane.showMessageDialog(this, "Please enter a valid phone number.", "Validation", JOptionPane.WARNING_MESSAGE); - return; - } - - // Save both the free-form notes field on ProgressSession and structured ContactLog row - com.studentgui.apphelpers.Database.saveSessionNotes(sessionId, notes); - com.studentgui.apphelpers.Database.saveContactLog(sessionId, this.studentNameParam, this.dateParam.toString(), guardian, method, phone, email, response, general, specific, notes); - LOG.info("Saved contact log for {}", studentNameParam); - com.studentgui.apphelpers.UiNotifier.show("Contact log saved."); - com.studentgui.apphelpers.dto.ContactPayload payload = new com.studentgui.apphelpers.dto.ContactPayload(sessionId, guardian, method, phone, email, response, general, specific, notes); - java.nio.file.Path jsonOut = com.studentgui.apphelpers.SessionJsonWriter.writeSessionJson(this.studentNameParam, "ContactLog", payload, sessionId); - if (jsonOut == null) { - LOG.warn("Unable to save ContactLog session JSON for sessionId={}", sessionId); - } - } catch (SQLException ex) { - LOG.error("Error saving contact log", ex); - javax.swing.JOptionPane.showMessageDialog(this, "Database error saving contact log: " + ex.getMessage(), "Database error", javax.swing.JOptionPane.ERROR_MESSAGE); - } - } -} +package com.studentgui.apppages; + +import java.awt.BorderLayout; +import java.awt.Font; +import java.awt.GridBagConstraints; +import java.awt.GridBagLayout; +import java.awt.Insets; +import java.awt.event.ActionEvent; +import java.awt.event.KeyEvent; +import java.sql.SQLException; +import java.time.LocalDate; + +import javax.swing.JButton; +import javax.swing.JComboBox; +import javax.swing.JLabel; +import javax.swing.JOptionPane; +import javax.swing.JPanel; +import javax.swing.JScrollPane; +import javax.swing.JTextArea; +import javax.swing.JTextField; +import javax.swing.SwingUtilities; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + * Structured parent/guardian contact log with validation and freeform notes. + * + *

Provides a comprehensive contact tracking form with structured fields for documenting + * communications with parents, guardians, and family members. Unlike the freeform notes pages + * ({@link SessionNotes}, {@link Observations}), this page captures both structured metadata + * and narrative details to support later reporting and documentation requirements.

+ * + *

Structured Fields:

+ *
    + *
  • Guardian Name: Full name of the parent/guardian contacted
  • + *
  • Contact Method: Dropdown selection (Phone, Email, In Person, Other)
  • + *
  • Phone Number: Contact phone number (validated format: 7-20 chars, digits/+/()-/space)
  • + *
  • Email Address: Contact email (validated format: basic email regex pattern)
  • + *
  • Contact Response: Brief summary of the guardian's response or concerns
  • + *
  • Contact General: High-level topic or category of the contact (e.g., "Progress Update", "IEP Discussion")
  • + *
  • Contact Specific: Specific items discussed or action points (e.g., "Discussed Braille materials order")
  • + *
  • Notes: Multi-line freeform notes area for detailed narrative
  • + *
+ * + *

Validation and Error Handling:

+ *
    + *
  • Email validation: Triggers warning if Contact Method is "Email" and email field doesn't match {@code ^[^@\s]+@[^@\s]+\.[^@\s]+$}
  • + *
  • Phone validation: Triggers warning if Contact Method is "Phone" and phone doesn't match {@code ^[0-9+()\-\s]{7,20}$}
  • + *
  • Validation failures display warning dialogs and do not persist data until corrected
  • + *
+ * + *

Data Persistence:

+ *
    + *
  • Structured fields persisted via {@link com.studentgui.apphelpers.Database#saveContactLog} to {@code ContactLog} table
  • + *
  • Notes also saved to {@code ProgressSession.notes} column via {@link com.studentgui.apphelpers.Database#saveSessionNotes}
  • + *
  • JSON export: {@code StudentDataFiles//Sessions/ContactLog/ContactLog--.json}
  • + *
  • Load Last Contact button retrieves most recent contact record via {@link com.studentgui.apphelpers.Database#fetchLatestContactLog}
  • + *
+ * + *

No plots are generated (contact logs are non-quantitative). The shared {@link JLineGraph} component + * is absent from this page's layout. This page does not implement listener interfaces and operates + * on static student/date parameters.

+ * + * @see com.studentgui.apphelpers.Database#saveContactLog + * @see com.studentgui.apphelpers.Database#fetchLatestContactLog + * @see com.studentgui.apphelpers.dto.ContactPayload + */ +public class ContactLog extends JPanel { + private static final Logger LOG = LoggerFactory.getLogger(ContactLog.class); + /** Text area where the user enters contact notes for the selected student. */ + private final JTextArea notesArea; + // additional contact fields + /** Guardian or parent name associated with the student. */ + private final JTextField guardianField; + /** Phone number used for contact. */ + private final JTextField phoneField; + /** Email address used for contact. */ + private final JTextField emailField; + /** Method of contact (Phone/Email/In Person/Other). */ + private final JComboBox contactMethodCombo; + /** Short description of the response received during contact. */ + private final JTextField contactResponseField; + /** High-level/general contact notes (summary). */ + private final JTextField contactGeneralField; + /** Specific items or action points discussed during contact. */ + private final JTextField contactSpecificField; + + /** Selected student display name associated with this page instance (may be null). */ + private final String studentNameParam; + + /** Session date to associate with saved notes from this page. */ + private final LocalDate dateParam; + + /** + * Construct a ContactLog page for the provided student and date. + * + * @param studentName selected student display name (may be null) + * @param date session date to associate with saved notes + * @param graph shared graph component shown under the editor + */ + public ContactLog(String studentName, LocalDate date, JLineGraph graph) { + this.studentNameParam = (studentName == null || studentName.trim().isEmpty()) ? com.studentgui.apphelpers.Helpers.defaultStudent() : studentName; + this.dateParam = date; + setLayout(new BorderLayout()); + + JPanel p = new JPanel(new GridBagLayout()); + JPanel view = new JPanel(new BorderLayout()); + view.add(p, BorderLayout.NORTH); + view.setBorder(javax.swing.BorderFactory.createEmptyBorder(20,20,20,20)); + JScrollPane scroll = new JScrollPane(view); + scroll.getAccessibleContext().setAccessibleName("Contact Log data entry scroll pane"); + GridBagConstraints gbc = new GridBagConstraints(); gbc.insets=new Insets(2,2,2,2); gbc.fill = GridBagConstraints.HORIZONTAL; gbc.anchor = GridBagConstraints.NORTHWEST; + JLabel title = new JLabel("Contact Log"); title.setFont(title.getFont().deriveFont(Font.BOLD,16)); title.setHorizontalAlignment(JLabel.LEFT); gbc.gridx=0; gbc.gridy=0; gbc.gridwidth=2; p.add(title, gbc); + + // Structured contact fields (placed above notes) + int row = 1; + gbc.gridwidth = 1; + int globalLabel = com.studentgui.uicomp.PhaseScoreField.getGlobalLabelWidth(); + gbc.gridx = 0; gbc.gridy = row; JLabel guardianLabel = new JLabel("Guardian Name:"); guardianLabel.setPreferredSize(new java.awt.Dimension(globalLabel, guardianLabel.getPreferredSize().height)); p.add(guardianLabel, gbc); + guardianField = new JTextField(24); guardianField.setName("contactlog_guardian"); gbc.gridx = 1; p.add(guardianField, gbc); + row++; + gbc.gridx = 0; gbc.gridy = row; JLabel methodLabel = new JLabel("Contact Method:"); methodLabel.setPreferredSize(new java.awt.Dimension(globalLabel, methodLabel.getPreferredSize().height)); p.add(methodLabel, gbc); + contactMethodCombo = new JComboBox<>(new String[]{"Phone","Email","In Person","Other"}); contactMethodCombo.setName("contactlog_method"); gbc.gridx = 1; p.add(contactMethodCombo, gbc); + row++; + gbc.gridx = 0; gbc.gridy = row; JLabel phoneLabel = new JLabel("Phone Number:"); phoneLabel.setPreferredSize(new java.awt.Dimension(globalLabel, phoneLabel.getPreferredSize().height)); p.add(phoneLabel, gbc); + phoneField = new JTextField(18); phoneField.setName("contactlog_phone"); gbc.gridx = 1; p.add(phoneField, gbc); + row++; + gbc.gridx = 0; gbc.gridy = row; JLabel emailLabel = new JLabel("Email Address:"); emailLabel.setPreferredSize(new java.awt.Dimension(globalLabel, emailLabel.getPreferredSize().height)); p.add(emailLabel, gbc); + emailField = new JTextField(24); emailField.setName("contactlog_email"); gbc.gridx = 1; p.add(emailField, gbc); + row++; + gbc.gridx = 0; gbc.gridy = row; JLabel responseLabel = new JLabel("Contact Response:"); responseLabel.setPreferredSize(new java.awt.Dimension(globalLabel, responseLabel.getPreferredSize().height)); p.add(responseLabel, gbc); + contactResponseField = new JTextField(24); contactResponseField.setName("contactlog_response"); gbc.gridx = 1; p.add(contactResponseField, gbc); + row++; + gbc.gridx = 0; gbc.gridy = row; JLabel generalLabel = new JLabel("Contact General:"); generalLabel.setPreferredSize(new java.awt.Dimension(globalLabel, generalLabel.getPreferredSize().height)); p.add(generalLabel, gbc); + contactGeneralField = new JTextField(24); contactGeneralField.setName("contactlog_general"); gbc.gridx = 1; p.add(contactGeneralField, gbc); + row++; + gbc.gridx = 0; gbc.gridy = row; JLabel specificLabel = new JLabel("Contact Specific:"); specificLabel.setPreferredSize(new java.awt.Dimension(globalLabel, specificLabel.getPreferredSize().height)); p.add(specificLabel, gbc); + contactSpecificField = new JTextField(24); contactSpecificField.setName("contactlog_specific"); gbc.gridx = 1; p.add(contactSpecificField, gbc); + row++; + + // Notes label + text area with accessibility + gbc.gridx = 0; gbc.gridy = row; gbc.gridwidth = 2; JLabel notesLabel = new JLabel("Notes:"); notesLabel.setPreferredSize(new java.awt.Dimension(globalLabel, notesLabel.getPreferredSize().height)); p.add(notesLabel, gbc); + row++; + + gbc.gridx = 0; gbc.gridy = row; gbc.gridwidth = 2; notesArea = new JTextArea(8,40); notesArea.setLineWrap(true); notesArea.setWrapStyleWord(true); notesArea.setToolTipText("Enter contact notes for the student"); notesArea.getAccessibleContext().setAccessibleName("Contact notes"); JScrollPane notesScroll = new JScrollPane(notesArea); notesScroll.setHorizontalScrollBarPolicy(JScrollPane.HORIZONTAL_SCROLLBAR_NEVER); p.add(notesScroll, gbc); + notesArea.setName("contactlog_notes"); + notesLabel.setLabelFor(notesArea); + + row++; + gbc.gridx = 0; gbc.gridy = row; gbc.gridwidth = 1; JButton save = new JButton("Save Contact"); + save.addActionListener((ActionEvent e)-> saveContact()); + save.setToolTipText("Save contact notes to the database"); + save.setMnemonic(KeyEvent.VK_S); + save.getAccessibleContext().setAccessibleName("Save Contact Notes"); + save.setName("contactlog_save"); + p.add(save, gbc); + + gbc.gridx = 1; JButton load = new JButton("Load Last Contact"); + load.addActionListener((ActionEvent e) -> loadLastContact()); + load.setToolTipText("Load the most recent contact for the selected student"); + load.setName("contactlog_load"); + p.add(load, gbc); + + add(scroll, BorderLayout.CENTER); + + SwingUtilities.invokeLater(()->{ p.setPreferredSize(p.getPreferredSize()); revalidate(); }); + + com.studentgui.apphelpers.Helpers.createFolderHierarchy(); + } + + private void loadLastContact() { + try { + com.studentgui.apphelpers.dto.ContactPayload p = com.studentgui.apphelpers.Database.fetchLatestContactLog(this.studentNameParam); + if (p == null) { + com.studentgui.apphelpers.UiNotifier.show("No contact found for this student."); + return; + } + guardianField.setText(p.guardian != null ? p.guardian : ""); + String method = p.method != null ? p.method : ""; + if (method != null) { + contactMethodCombo.setSelectedItem(method); + } + phoneField.setText(p.phone != null ? p.phone : ""); + emailField.setText(p.email != null ? p.email : ""); + contactResponseField.setText(p.response != null ? p.response : ""); + contactGeneralField.setText(p.general != null ? p.general : ""); + contactSpecificField.setText(p.specific != null ? p.specific : ""); + notesArea.setText(p.notes != null ? p.notes : ""); + } catch (SQLException ex) { + LOG.error("Error loading last contact", ex); + JOptionPane.showMessageDialog(this, "Database error loading contact: " + ex.getMessage(), "Database error", JOptionPane.ERROR_MESSAGE); + } + } + + /** + * Persist the contact notes entered into the notes area as a session note + * for the selected student. Shows a confirmation dialog on success and + * error dialogs on failure. + */ + private void saveContact() { + try { + int studentId = com.studentgui.apphelpers.Database.getOrCreateStudent(this.studentNameParam); + int ptId = com.studentgui.apphelpers.Database.getOrCreateProgressType("ContactLog"); + int sessionId = com.studentgui.apphelpers.Database.createProgressSession(studentId, ptId, this.dateParam); + + String notes = notesArea.getText(); + String guardian = guardianField.getText(); + String method = (String) contactMethodCombo.getSelectedItem(); + String phone = phoneField.getText(); + String email = emailField.getText(); + String response = contactResponseField.getText(); + String general = contactGeneralField.getText(); + String specific = contactSpecificField.getText(); + + // Basic validation + if (method != null && method.equals("Email") && (email == null || !email.matches("^[^@\\s]+@[^@\\s]+\\.[^@\\s]+$"))) { + JOptionPane.showMessageDialog(this, "Please enter a valid email address.", "Validation", JOptionPane.WARNING_MESSAGE); + return; + } + if (method != null && method.equals("Phone") && (phone == null || !phone.matches("^[0-9+()\\-\s]{7,20}$"))) { + JOptionPane.showMessageDialog(this, "Please enter a valid phone number.", "Validation", JOptionPane.WARNING_MESSAGE); + return; + } + + // Save both the free-form notes field on ProgressSession and structured ContactLog row + com.studentgui.apphelpers.Database.saveSessionNotes(sessionId, notes); + com.studentgui.apphelpers.Database.saveContactLog(sessionId, this.studentNameParam, this.dateParam.toString(), guardian, method, phone, email, response, general, specific, notes); + LOG.info("Saved contact log for {}", studentNameParam); + com.studentgui.apphelpers.UiNotifier.show("Contact log saved."); + com.studentgui.apphelpers.dto.ContactPayload payload = new com.studentgui.apphelpers.dto.ContactPayload(sessionId, guardian, method, phone, email, response, general, specific, notes); + java.nio.file.Path jsonOut = com.studentgui.apphelpers.SessionJsonWriter.writeSessionJson(this.studentNameParam, "ContactLog", payload, sessionId); + if (jsonOut == null) { + LOG.warn("Unable to save ContactLog session JSON for sessionId={}", sessionId); + } + } catch (SQLException ex) { + LOG.error("Error saving contact log", ex); + javax.swing.JOptionPane.showMessageDialog(this, "Database error saving contact log: " + ex.getMessage(), "Database error", javax.swing.JOptionPane.ERROR_MESSAGE); + } + } +} diff --git a/src/main/java/com/studentgui/apppages/DigitalLiteracy.java b/src/main/java/com/studentgui/apppages/DigitalLiteracy.java index 2be901d..6115b28 100644 --- a/src/main/java/com/studentgui/apppages/DigitalLiteracy.java +++ b/src/main/java/com/studentgui/apppages/DigitalLiteracy.java @@ -1,387 +1,457 @@ -package com.studentgui.apppages; - -import java.awt.BorderLayout; -import java.awt.Font; -import java.awt.GridBagConstraints; -import java.awt.GridBagLayout; -import java.awt.Insets; -import java.awt.event.ActionEvent; -import java.awt.event.KeyEvent; -import java.sql.SQLException; -import java.time.LocalDate; -import java.util.List; - -import javax.swing.JButton; -import javax.swing.JLabel; -import javax.swing.JOptionPane; -import javax.swing.JPanel; -import javax.swing.JScrollPane; -import javax.swing.SwingUtilities; - -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; - -/** - * Digital literacy skills progression page UI. - *

- * Presents a set of numeric input fields for digital literacy skills and - * persists entries to the normalized database. A provided {@link JLineGraph} - * instance is used to visualize recent assessment sessions. - *

- */ -public class DigitalLiteracy extends JPanel implements com.studentgui.app.DateChangeListener, com.studentgui.app.StudentChangeListener { - private static final Logger LOG = LoggerFactory.getLogger(DigitalLiteracy.class); - /** Array of input fields for each digital literacy skill part. */ - private final com.studentgui.uicomp.PhaseScoreField[] skillFields; - /** Canonical list of digital literacy assessment parts: code and display label. */ - private final String[][] parts; - - /** Shared graph used to visualize recent digital literacy sessions. */ - private final JLineGraph lineGraph; // Reference to the JLineGraph instance - - /** Selected student's display name (may be null) for saving/fetching data. */ - private String studentNameParam; - /** Title label shown at the top of the Digital Literacy page. */ - private JLabel titleLabel; - /** Base title text for the page; used when building the header string. */ - private final String baseTitle = "Digital Literacy Skills Progression"; - - /** Session date to associate with persisted digital literacy progress. */ - private LocalDate dateParam; - - /** - * Construct the Digital Literacy page for the given student and date. - * - * @param studentName display name of the selected student (may be null) - * @param date session date to associate with persisted progress - * @param lineGraph shared graph component used to display recent results - */ - public DigitalLiteracy(final String studentName, final LocalDate date, final JLineGraph lineGraph) { - this.studentNameParam = (studentName == null || studentName.trim().isEmpty()) ? com.studentgui.apphelpers.Helpers.defaultStudent() : studentName; - this.dateParam = date; - this.lineGraph = lineGraph; // Use the passed in graph instance - setLayout(new BorderLayout()); - - this.parts = new String[][]{ - {"P1_1","1.1 Turn Device On/Off"},{"P1_2","1.2 Turn VoiceOver On/Off"},{"P1_3","1.3 Gestures to Click Icons"},{"P1_4","1.4 Home Screen Icons to Open Documents"},{"P1_5","1.5 Save Documents"},{"P1_6","1.6 Online Tools/Resources"},{"P1_7","1.7 Keyboarding"},{"P1_8","1.8 Use Different Elements"},{"P1_9","1.9 Control Center, App Switcher..."}, - {"P2_1","2.1 Write, edit save"},{"P2_2","2.2 Read, Navigate Document"},{"P2_3","2.3 Use Menubar"},{"P2_4","2.4 Highlight text, copy and paste text"},{"P2_5","2.5 Copy and paste images"},{"P2_6","2.6 Proofread and edit"}, - {"P3_1","3.1 Describe Spreadsheet"},{"P3_2","3.2 Explain terms and concepts"},{"P3_3","3.3 Enter/Edit data"}, - {"P4_1","4.1 Presentation Tools"},{"P4_2","4.2 Create Slides"},{"P4_3","4.3 Edit Slides"},{"P4_4","4.4 Present Slides"},{"P4_5","4.5 Share Slides"}, - {"P5_1","5.1 Acceptable Use"},{"P5_2","5.2 Digital Citizenship"},{"P5_3","5.3 Internet Safety"},{"P5_4","5.4 Copyright"},{"P5_5","5.5 Plagiarism"} - }; - - // Panel for data entry - JPanel dataEntryPanel = new JPanel(); - dataEntryPanel.setLayout(new GridBagLayout()); - JScrollPane dataEntryScrollPane = new JScrollPane(dataEntryPanel); - dataEntryScrollPane.setVerticalScrollBarPolicy(JScrollPane.VERTICAL_SCROLLBAR_AS_NEEDED); - dataEntryScrollPane.setHorizontalScrollBarPolicy(JScrollPane.HORIZONTAL_SCROLLBAR_NEVER); - - GridBagConstraints gbc = new GridBagConstraints(); - gbc.insets = new Insets(2, 2, 2, 2); - gbc.fill = GridBagConstraints.HORIZONTAL; - gbc.weightx = 1.0; - gbc.weighty = 0.0; - - this.titleLabel = new JLabel(baseTitle); - this.titleLabel.setFont(this.titleLabel.getFont().deriveFont(Font.BOLD, 16)); - gbc.gridx = 0; - gbc.gridy = 0; - gbc.gridwidth = GridBagConstraints.REMAINDER; - dataEntryPanel.add(this.titleLabel, gbc); - - gbc.gridy = 1; - gbc.gridwidth = GridBagConstraints.REMAINDER; - gbc.ipady = 20; - dataEntryPanel.add(new JPanel(), gbc); - - // layout spacing handled by PhaseScoreField - - String[] labels = java.util.Arrays.stream(parts).map(x->x[1]).toArray(String[]::new); - int maxPx = com.studentgui.uicomp.PhaseScoreField.computeMaxLabelPixelWidth(titleLabel.getFont(), labels); - com.studentgui.uicomp.PhaseScoreField.setGlobalLabelWidth(Math.min(320, Math.max(140, maxPx + 50))); - skillFields = new com.studentgui.uicomp.PhaseScoreField[this.parts.length]; - for (int i = 0; i < this.parts.length; i++) { - gbc.gridy = i + 2; - gbc.gridx = 0; - gbc.gridwidth = 1; - com.studentgui.uicomp.PhaseScoreField field = new com.studentgui.uicomp.PhaseScoreField(parts[i][1], 0); - field.setName("digitalliteracy_" + this.parts[i][0]); - field.getAccessibleContext().setAccessibleName(this.parts[i][1]); - field.setToolTipText("Enter whole number score for " + this.parts[i][1]); - gbc.gridx = 0; gbc.gridwidth = 2; gbc.insets = new Insets(5, 5, 5, 5); - dataEntryPanel.add(field, gbc); - skillFields[i] = field; - gbc.gridx = 2; gbc.gridwidth = 1; gbc.insets = new Insets(5, 0, 5, 5); - dataEntryPanel.add(new JPanel(), gbc); - } - - gbc.gridy = this.parts.length + 3; - gbc.gridx = 0; - gbc.gridwidth = GridBagConstraints.REMAINDER; - gbc.weighty = 1.0; - dataEntryPanel.add(new JPanel(), gbc); - - // Place Submit and Open Latest side-by-side and match IOS button height - gbc.gridy = this.parts.length + 4; - gbc.weighty = 0.0; - gbc.gridx = 0; - gbc.gridwidth = 1; - JButton submitDataButton = new JButton("Submit Data"); - submitDataButton.setPreferredSize(new java.awt.Dimension(0, 32)); - submitDataButton.addActionListener((ActionEvent e) -> { submitData(); refreshGraph(); }); - submitDataButton.setToolTipText("Save digital literacy scores for the selected student (Alt+S)"); - submitDataButton.setMnemonic(KeyEvent.VK_S); - submitDataButton.getAccessibleContext().setAccessibleName("Submit Digital Literacy Data"); - submitDataButton.setName("digitalliteracy_submit"); - dataEntryPanel.add(submitDataButton, gbc); - - gbc.gridx = 1; - JButton openLatestBtn = new JButton("Open Latest Plot"); - openLatestBtn.setPreferredSize(new java.awt.Dimension(0, 32)); - openLatestBtn.addActionListener((ActionEvent e) -> { - java.nio.file.Path p = com.studentgui.apphelpers.Helpers.latestPlotPath(this.studentNameParam, "DigitalLiteracy"); - if (p == null) { - com.studentgui.apphelpers.UiNotifier.show("No DigitalLiteracy plot found for student"); - } else { - try { - java.awt.Desktop.getDesktop().open(p.toFile()); - } catch (java.io.IOException | UnsupportedOperationException | SecurityException ex) { - com.studentgui.apphelpers.UiNotifier.show("Unable to open plot: " + p.getFileName().toString()); - } - } - }); - dataEntryPanel.add(openLatestBtn, gbc); - - gbc.gridx = 2; gbc.gridwidth = GridBagConstraints.REMAINDER; - dataEntryPanel.add(new JPanel(), gbc); - - dataEntryScrollPane.getAccessibleContext().setAccessibleName("Digital Literacy data entry scroll pane"); - - add(dataEntryScrollPane, BorderLayout.CENTER); - - // Add existing graph reference - add(lineGraph, BorderLayout.SOUTH); - - SwingUtilities.invokeLater(() -> { - dataEntryPanel.setPreferredSize(dataEntryPanel.getPreferredSize()); - updateTitleDate(); - revalidate(); - }); - - // Ensure application folders and DB schema exist - com.studentgui.apphelpers.Helpers.createFolderHierarchy(); - initDatabase(); - refreshGraph(); - } - - /** - * Ensure the progress type and assessment parts for DigitalLiteracy exist - * in the canonical schema. - */ - private void initDatabase() { - try { - int ptId = com.studentgui.apphelpers.Database.getOrCreateProgressType("DigitalLiteracy"); - // Use canonical part codes from this.parts - String[] codes = new String[this.parts.length]; - for (int i = 0; i < this.parts.length; i++) { - codes[i] = this.parts[i][0]; - } - com.studentgui.apphelpers.Database.ensureAssessmentParts(ptId, codes); - } catch (SQLException e) { - LOG.error("SQL error ensuring assessment parts for DigitalLiteracy", e); - } - } - - /** - * Validate and persist input field values as a new progress session for - * the selected student. - */ - private void submitData() { - if (studentNameParam == null || studentNameParam.trim().isEmpty()) { - JOptionPane.showMessageDialog(this, "Please select a student before submitting Digital Literacy data.", "Missing student", JOptionPane.WARNING_MESSAGE); - return; - } - - try { - int studentId = com.studentgui.apphelpers.Database.getOrCreateStudent(studentNameParam); - int ptId = com.studentgui.apphelpers.Database.getOrCreateProgressType("DigitalLiteracy"); - int sessionId = com.studentgui.apphelpers.Database.createProgressSession(studentId, ptId, dateParam); - - String[] codes = new String[this.parts.length]; - int[] scores = new int[this.parts.length]; - for (int i = 0; i < this.parts.length; i++) { - codes[i] = this.parts[i][0]; - scores[i] = skillFields[i].getValue(); - } - com.studentgui.apphelpers.Database.insertAssessmentResults(sessionId, ptId, codes, scores); - LOG.info("Data submitted successfully via normalized schema."); - com.studentgui.apphelpers.UiNotifier.show("Digital Literacy data saved."); - com.studentgui.apphelpers.dto.AssessmentPayload payload = new com.studentgui.apphelpers.dto.AssessmentPayload(sessionId, codes, scores); - java.nio.file.Path jsonOut = com.studentgui.apphelpers.SessionJsonWriter.writeSessionJson(this.studentNameParam, "DigitalLiteracy", payload, sessionId); - if (jsonOut == null) { - LOG.warn("Unable to save DigitalLiteracy session JSON for sessionId={}", sessionId); - } - try { - java.nio.file.Path plotsOut = com.studentgui.apphelpers.Helpers.studentPlotsDir(this.studentNameParam); - java.nio.file.Path reportsOut = com.studentgui.apphelpers.Helpers.studentReportsDir(this.studentNameParam); - java.nio.file.Files.createDirectories(plotsOut); - java.nio.file.Files.createDirectories(reportsOut); - java.time.format.DateTimeFormatter df = java.time.format.DateTimeFormatter.ISO_DATE; - String dateStr = this.dateParam != null ? this.dateParam.format(df) : java.time.LocalDate.now().toString(); - String baseName = "DigitalLiteracy-" + sessionId + "-" + dateStr; - - com.studentgui.apphelpers.Database.ResultsWithDates rwd = com.studentgui.apphelpers.Database.fetchLatestAssessmentResultsWithDates(this.studentNameParam, "DigitalLiteracy", Integer.MAX_VALUE); - java.util.Map groups = null; - String[] labels = new String[this.parts.length]; - for (int i = 0; i < this.parts.length; i++) { - labels[i] = this.parts[i][1]; - } - if (rwd != null && rwd.rows != null && !rwd.rows.isEmpty()) { - lineGraph.updateWithGroupedDataByDate(rwd.dates, rwd.rows, codes, labels); - groups = lineGraph.saveGroupedCharts(plotsOut, baseName, 1000, 240); - java.time.LocalDate headerDate = rwd.dates.get(rwd.dates.size() - 1); - dateStr = headerDate.format(df); - } else { - java.util.List> rowsList = new java.util.ArrayList<>(); - java.util.List latest = new java.util.ArrayList<>(); - for (int v : scores) latest.add(v); - rowsList.add(latest); - lineGraph.updateWithGroupedData(rowsList, codes); - groups = lineGraph.saveGroupedCharts(plotsOut, baseName, 1000, 240); - } - - if (groups == null) { - groups = new java.util.LinkedHashMap<>(); - } - StringBuilder md = new StringBuilder(); - md.append("# ").append(this.studentNameParam == null ? "Unknown Student" : this.studentNameParam).append(" - ").append(dateStr).append("\n\n"); - for (java.util.Map.Entry e : groups.entrySet()) { - md.append("## ").append(e.getKey()).append("\n\n"); - md.append("![](../plots/").append(e.getValue().getFileName().toString()).append(")\n\n"); - } - java.nio.file.Path mdFile = reportsOut.resolve(baseName + ".md"); - java.nio.file.Files.writeString(mdFile, md.toString(), java.nio.charset.StandardCharsets.UTF_8); - - try { - String[] palette = JLineGraph.PALETTE_HEX; - java.util.LinkedHashMap> groupsIdx = new java.util.LinkedHashMap<>(); - for (int i = 0; i < codes.length; i++) { - String code = codes[i]; - String grp = code != null && code.contains("_") ? code.split("_")[0] : code; - groupsIdx.computeIfAbsent(grp, k -> new java.util.ArrayList<>()).add(i); - } - StringBuilder html = new StringBuilder(); - html.append(""); - html.append(this.studentNameParam == null ? "Student Report" : this.studentNameParam).append(" - ").append(dateStr).append(""); - html.append(""); - html.append(""); - html.append("

").append(this.studentNameParam == null ? "Unknown Student" : this.studentNameParam).append(" - ").append(dateStr).append("

"); - for (java.util.Map.Entry e2 : groups.entrySet()) { - String grp = e2.getKey(); - String imgName = e2.getValue().getFileName().toString(); - html.append("

").append(grp).append("

"); - html.append("
\"").append(grp).append("\"
"); - java.util.List idxs = groupsIdx.getOrDefault(grp, new java.util.ArrayList<>()); - html.append("
"); - for (int s = 0; s < idxs.size(); s++) { - int idx = idxs.get(s); - String code = codes[idx]; - String human = this.parts[idx][1]; - String seriesName = code + " - " + human; - String color = palette[s % palette.length]; - html.append("
"); - html.append(""); - html.append("
"); - html.append(seriesName); - html.append("
"); - } - html.append("
"); - } - html.append(""); - java.nio.file.Path htmlFile = reportsOut.resolve(baseName + ".html"); - java.nio.file.Files.writeString(htmlFile, html.toString(), java.nio.charset.StandardCharsets.UTF_8); - LOG.info("Wrote DigitalLiteracy HTML session report {}", htmlFile); - } catch (java.io.IOException ioex) { - LOG.warn("Unable to write DigitalLiteracy HTML report: {}", ioex.toString()); - } - } catch (java.io.IOException ioe) { - LOG.warn("Unable to save DigitalLiteracy per-phase plots or markdown report: {}", ioe.toString()); - } - } catch (SQLException e) { - LOG.error("SQL error submitting Digital Literacy data", e); - JOptionPane.showMessageDialog(this, "Database error saving Digital Literacy data: " + e.getMessage(), "Database error", JOptionPane.ERROR_MESSAGE); - } - } - - /** - * Load recent assessment sessions and update the shared {@link JLineGraph} - * component with the returned values. - */ - private void refreshGraph() { - try { - List> allSkillValues = com.studentgui.apphelpers.Database.fetchLatestAssessmentResults(studentNameParam, "DigitalLiteracy", 5); - if (allSkillValues != null && !allSkillValues.isEmpty()) { - // Build canonical codes array in the same order used when ensuring parts - String[] codes = new String[this.parts.length]; - for (int i = 0; i < this.parts.length; i++) { - codes[i] = this.parts[i][0]; - } - lineGraph.updateWithGroupedData(allSkillValues, codes); - // Write to the consolidated per-run data dumps file when enabled - if (Boolean.parseBoolean(com.studentgui.apphelpers.Settings.get("dump.enabled", "false"))) { - try { - String appHome = System.getProperty("APP_HOME", com.studentgui.apphelpers.Helpers.APP_HOME.toString()); - String ts = System.getProperty("LOG_TS", String.valueOf(java.time.Instant.now().getEpochSecond())); - java.nio.file.Path logDir = java.nio.file.Paths.get(appHome).resolve("logs"); - java.nio.file.Files.createDirectories(logDir); - java.nio.file.Path logFile = logDir.resolve("data_dumps_" + ts + ".log"); - StringBuilder sb = new StringBuilder(); - sb.append("[DigitalLiteracy]").append(System.lineSeparator()); - sb.append(java.time.Instant.now().toString()).append(" - student=").append(this.studentNameParam).append(System.lineSeparator()); - sb.append("data=").append(allSkillValues.toString()).append(System.lineSeparator()); - sb.append(System.lineSeparator()); - java.nio.file.Files.writeString(logFile, sb.toString(), java.nio.charset.StandardCharsets.UTF_8, java.nio.file.StandardOpenOption.CREATE, java.nio.file.StandardOpenOption.APPEND); - } catch (java.io.IOException ioe) { - LOG.trace("Unable to write DigitalLiteracy load log: {}", ioe.toString()); - } - } - } else { - LOG.info("No data to plot."); - } - } catch (SQLException e) { - LOG.error("SQL error refreshing Digital Literacy graph", e); - } - } - - @Override - public void dateChanged(final LocalDate newDate) { - this.dateParam = newDate; - SwingUtilities.invokeLater(() -> { - refreshGraph(); - updateTitleDate(); - }); - } - - @Override - public void studentChanged(final String newStudent) { - this.studentNameParam = newStudent; - SwingUtilities.invokeLater(() -> { - refreshGraph(); - updateTitleDate(); - }); - } - - private void updateTitleDate() { - try { - String dateStr = this.dateParam != null ? this.dateParam.toString() : java.time.LocalDate.now().toString(); - this.titleLabel.setText(baseTitle + " - " + dateStr); - } catch (Exception ex) { - this.titleLabel.setText(baseTitle); - } - } - - -} +package com.studentgui.apppages; + +import java.awt.BorderLayout; +import java.awt.Font; +import java.awt.GridBagConstraints; +import java.awt.GridBagLayout; +import java.awt.Insets; +import java.awt.event.ActionEvent; +import java.awt.event.KeyEvent; +import java.sql.SQLException; +import java.time.LocalDate; +import java.util.List; + +import javax.swing.JButton; +import javax.swing.JLabel; +import javax.swing.JOptionPane; +import javax.swing.JPanel; +import javax.swing.JScrollPane; +import javax.swing.SwingUtilities; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + * Digital literacy and computer skills assessment page. + * + *

Evaluates foundational technology competencies required for academic and professional + * success in digital environments. Covers 27 skills organized into 5 progressive competency domains:

+ * + *
    + *
  • Phase 1 (P1_1–P1_9): Device Basics and Navigation + *
      + *
    • Powering devices on/off, accessibility feature activation (VoiceOver/TalkBack/Narrator)
    • + *
    • Touch/mouse gestures for app launching and navigation
    • + *
    • Home screen organization, icon identification, and app launching
    • + *
    • Document creation, saving, and retrieval workflows
    • + *
    • Online resource access (web portals, learning management systems)
    • + *
    • Basic keyboarding (home row, touch typing fundamentals)
    • + *
    • UI element interaction (buttons, menus, text fields, sliders)
    • + *
    • System-level navigation (Control Center, App Switcher, Task Manager, Dock)
    • + *
    + *
  • + *
  • Phase 2 (P2_1–P2_6): Word Processing Fundamentals + *
      + *
    • Creating, editing, and saving text documents
    • + *
    • Reading and navigating documents using assistive technology or visual scanning
    • + *
    • Menu bar and toolbar interaction for formatting and commands
    • + *
    • Text selection, highlighting, copy/paste workflows
    • + *
    • Image insertion and manipulation (copy, paste, resize, position)
    • + *
    • Proofreading strategies and editing for clarity/correctness
    • + *
    + *
  • + *
  • Phase 3 (P3_1–P3_3): Spreadsheet Fundamentals + *
      + *
    • Describing spreadsheet structure (rows, columns, cells, sheets)
    • + *
    • Spreadsheet terminology (cell references, formulas, functions, ranges)
    • + *
    • Data entry and editing (typing, autofill, formula entry)
    • + *
    + *
  • + *
  • Phase 4 (P4_1–P4_5): Presentation Software + *
      + *
    • Presentation tool concepts (slides, layouts, templates)
    • + *
    • Creating structured presentations (title, content, transitions)
    • + *
    • Editing slides (text, formatting, reordering)
    • + *
    • Presenting slides effectively (presenter view, navigation, notes)
    • + *
    • Sharing presentations (export, cloud upload, email)
    • + *
    + *
  • + *
  • Phase 5 (P5_1–P5_5): Digital Citizenship and Ethics + *
      + *
    • Acceptable Use Policies (school/workplace technology guidelines)
    • + *
    • Digital citizenship principles (respectful communication, netiquette)
    • + *
    • Internet safety (phishing, malware, safe browsing)
    • + *
    • Copyright awareness (fair use, attribution, Creative Commons)
    • + *
    • Plagiarism recognition and avoidance (paraphrasing, citations, originality)
    • + *
    + *
  • + *
+ * + *

Data Persistence and Report Generation:

+ *
    + *
  • Scores captured via {@link com.studentgui.uicomp.PhaseScoreField} (integer 0–4 typical)
  • + *
  • Persisted to normalized schema via {@link com.studentgui.apphelpers.Database#insertAssessmentResults}
  • + *
  • JSON export: {@code StudentDataFiles//Sessions/DigitalLiteracy/DigitalLiteracy--.json}
  • + *
  • Phase-grouped time-series plots: {@code plots/DigitalLiteracy---P.png} (5 phase groups)
  • + *
  • Markdown and HTML reports with embedded plots and color-coded legends
  • + *
+ * + *

The shared {@link JLineGraph} visualizes recent session trends grouped by phase prefix. + * Implements {@link com.studentgui.app.DateChangeListener} and {@link com.studentgui.app.StudentChangeListener} + * for dynamic updates when global selections change.

+ * + *

Note: Skill codes and phases intentionally overlap with {@link IOS} to allow + * cross-platform skill mapping. Some assessment items are device-agnostic and track the same + * underlying competencies across iOS, Windows, macOS, and ChromeOS environments.

+ * + * @see com.studentgui.apphelpers.Database + * @see JLineGraph + * @see com.studentgui.uicomp.PhaseScoreField + * @see IOS + */ +public class DigitalLiteracy extends JPanel implements com.studentgui.app.DateChangeListener, com.studentgui.app.StudentChangeListener { + private static final Logger LOG = LoggerFactory.getLogger(DigitalLiteracy.class); + /** Array of input fields for each digital literacy skill part. */ + private final com.studentgui.uicomp.PhaseScoreField[] skillFields; + /** Canonical list of digital literacy assessment parts: code and display label. */ + private final String[][] parts; + + /** Shared graph used to visualize recent digital literacy sessions. */ + private final JLineGraph lineGraph; // Reference to the JLineGraph instance + + /** Selected student's display name (may be null) for saving/fetching data. */ + private String studentNameParam; + /** Title label shown at the top of the Digital Literacy page. */ + private JLabel titleLabel; + /** Base title text for the page; used when building the header string. */ + private final String baseTitle = "Digital Literacy Skills Progression"; + + /** Session date to associate with persisted digital literacy progress. */ + private LocalDate dateParam; + + /** + * Construct the Digital Literacy page for the given student and date. + * + * @param studentName display name of the selected student (may be null) + * @param date session date to associate with persisted progress + * @param lineGraph shared graph component used to display recent results + */ + public DigitalLiteracy(final String studentName, final LocalDate date, final JLineGraph lineGraph) { + this.studentNameParam = (studentName == null || studentName.trim().isEmpty()) ? com.studentgui.apphelpers.Helpers.defaultStudent() : studentName; + this.dateParam = date; + this.lineGraph = lineGraph; // Use the passed in graph instance + setLayout(new BorderLayout()); + + this.parts = new String[][]{ + {"P1_1","1.1 Turn Device On/Off"},{"P1_2","1.2 Turn VoiceOver On/Off"},{"P1_3","1.3 Gestures to Click Icons"},{"P1_4","1.4 Home Screen Icons to Open Documents"},{"P1_5","1.5 Save Documents"},{"P1_6","1.6 Online Tools/Resources"},{"P1_7","1.7 Keyboarding"},{"P1_8","1.8 Use Different Elements"},{"P1_9","1.9 Control Center, App Switcher..."}, + {"P2_1","2.1 Write, edit save"},{"P2_2","2.2 Read, Navigate Document"},{"P2_3","2.3 Use Menubar"},{"P2_4","2.4 Highlight text, copy and paste text"},{"P2_5","2.5 Copy and paste images"},{"P2_6","2.6 Proofread and edit"}, + {"P3_1","3.1 Describe Spreadsheet"},{"P3_2","3.2 Explain terms and concepts"},{"P3_3","3.3 Enter/Edit data"}, + {"P4_1","4.1 Presentation Tools"},{"P4_2","4.2 Create Slides"},{"P4_3","4.3 Edit Slides"},{"P4_4","4.4 Present Slides"},{"P4_5","4.5 Share Slides"}, + {"P5_1","5.1 Acceptable Use"},{"P5_2","5.2 Digital Citizenship"},{"P5_3","5.3 Internet Safety"},{"P5_4","5.4 Copyright"},{"P5_5","5.5 Plagiarism"} + }; + + // Panel for data entry + JPanel dataEntryPanel = new JPanel(); + dataEntryPanel.setLayout(new GridBagLayout()); + JScrollPane dataEntryScrollPane = new JScrollPane(dataEntryPanel); + dataEntryScrollPane.setVerticalScrollBarPolicy(JScrollPane.VERTICAL_SCROLLBAR_AS_NEEDED); + dataEntryScrollPane.setHorizontalScrollBarPolicy(JScrollPane.HORIZONTAL_SCROLLBAR_NEVER); + + GridBagConstraints gbc = new GridBagConstraints(); + gbc.insets = new Insets(2, 2, 2, 2); + gbc.fill = GridBagConstraints.HORIZONTAL; + gbc.weightx = 1.0; + gbc.weighty = 0.0; + + this.titleLabel = new JLabel(baseTitle); + this.titleLabel.setFont(this.titleLabel.getFont().deriveFont(Font.BOLD, 16)); + gbc.gridx = 0; + gbc.gridy = 0; + gbc.gridwidth = GridBagConstraints.REMAINDER; + dataEntryPanel.add(this.titleLabel, gbc); + + gbc.gridy = 1; + gbc.gridwidth = GridBagConstraints.REMAINDER; + gbc.ipady = 20; + dataEntryPanel.add(new JPanel(), gbc); + + // layout spacing handled by PhaseScoreField + + String[] labels = java.util.Arrays.stream(parts).map(x->x[1]).toArray(String[]::new); + int maxPx = com.studentgui.uicomp.PhaseScoreField.computeMaxLabelPixelWidth(titleLabel.getFont(), labels); + com.studentgui.uicomp.PhaseScoreField.setGlobalLabelWidth(Math.min(320, Math.max(140, maxPx + 50))); + skillFields = new com.studentgui.uicomp.PhaseScoreField[this.parts.length]; + for (int i = 0; i < this.parts.length; i++) { + gbc.gridy = i + 2; + gbc.gridx = 0; + gbc.gridwidth = 1; + com.studentgui.uicomp.PhaseScoreField field = new com.studentgui.uicomp.PhaseScoreField(parts[i][1], 0); + field.setName("digitalliteracy_" + this.parts[i][0]); + field.getAccessibleContext().setAccessibleName(this.parts[i][1]); + field.setToolTipText("Enter whole number score for " + this.parts[i][1]); + gbc.gridx = 0; gbc.gridwidth = 2; gbc.insets = new Insets(5, 5, 5, 5); + dataEntryPanel.add(field, gbc); + skillFields[i] = field; + gbc.gridx = 2; gbc.gridwidth = 1; gbc.insets = new Insets(5, 0, 5, 5); + dataEntryPanel.add(new JPanel(), gbc); + } + + gbc.gridy = this.parts.length + 3; + gbc.gridx = 0; + gbc.gridwidth = GridBagConstraints.REMAINDER; + gbc.weighty = 1.0; + dataEntryPanel.add(new JPanel(), gbc); + + // Place Submit and Open Latest side-by-side and match IOS button height + gbc.gridy = this.parts.length + 4; + gbc.weighty = 0.0; + gbc.gridx = 0; + gbc.gridwidth = 1; + JButton submitDataButton = new JButton("Submit Data"); + submitDataButton.setPreferredSize(new java.awt.Dimension(0, 32)); + submitDataButton.addActionListener((ActionEvent e) -> { submitData(); refreshGraph(); }); + submitDataButton.setToolTipText("Save digital literacy scores for the selected student (Alt+S)"); + submitDataButton.setMnemonic(KeyEvent.VK_S); + submitDataButton.getAccessibleContext().setAccessibleName("Submit Digital Literacy Data"); + submitDataButton.setName("digitalliteracy_submit"); + dataEntryPanel.add(submitDataButton, gbc); + + gbc.gridx = 1; + JButton openLatestBtn = new JButton("Open Latest Plot"); + openLatestBtn.setPreferredSize(new java.awt.Dimension(0, 32)); + openLatestBtn.addActionListener((ActionEvent e) -> { + java.nio.file.Path p = com.studentgui.apphelpers.Helpers.latestPlotPath(this.studentNameParam, "DigitalLiteracy"); + if (p == null) { + com.studentgui.apphelpers.UiNotifier.show("No DigitalLiteracy plot found for student"); + } else { + try { + java.awt.Desktop.getDesktop().open(p.toFile()); + } catch (java.io.IOException | UnsupportedOperationException | SecurityException ex) { + com.studentgui.apphelpers.UiNotifier.show("Unable to open plot: " + p.getFileName().toString()); + } + } + }); + dataEntryPanel.add(openLatestBtn, gbc); + + gbc.gridx = 2; gbc.gridwidth = GridBagConstraints.REMAINDER; + dataEntryPanel.add(new JPanel(), gbc); + + dataEntryScrollPane.getAccessibleContext().setAccessibleName("Digital Literacy data entry scroll pane"); + + add(dataEntryScrollPane, BorderLayout.CENTER); + + // Add existing graph reference + add(lineGraph, BorderLayout.SOUTH); + + SwingUtilities.invokeLater(() -> { + dataEntryPanel.setPreferredSize(dataEntryPanel.getPreferredSize()); + updateTitleDate(); + revalidate(); + }); + + // Ensure application folders and DB schema exist + com.studentgui.apphelpers.Helpers.createFolderHierarchy(); + initDatabase(); + refreshGraph(); + } + + /** + * Ensure the progress type and assessment parts for DigitalLiteracy exist + * in the canonical schema. + */ + private void initDatabase() { + try { + int ptId = com.studentgui.apphelpers.Database.getOrCreateProgressType("DigitalLiteracy"); + // Use canonical part codes from this.parts + String[] codes = new String[this.parts.length]; + for (int i = 0; i < this.parts.length; i++) { + codes[i] = this.parts[i][0]; + } + com.studentgui.apphelpers.Database.ensureAssessmentParts(ptId, codes); + } catch (SQLException e) { + LOG.error("SQL error ensuring assessment parts for DigitalLiteracy", e); + } + } + + /** + * Validate and persist input field values as a new progress session for + * the selected student. + */ + private void submitData() { + if (studentNameParam == null || studentNameParam.trim().isEmpty()) { + JOptionPane.showMessageDialog(this, "Please select a student before submitting Digital Literacy data.", "Missing student", JOptionPane.WARNING_MESSAGE); + return; + } + + try { + int studentId = com.studentgui.apphelpers.Database.getOrCreateStudent(studentNameParam); + int ptId = com.studentgui.apphelpers.Database.getOrCreateProgressType("DigitalLiteracy"); + int sessionId = com.studentgui.apphelpers.Database.createProgressSession(studentId, ptId, dateParam); + + String[] codes = new String[this.parts.length]; + int[] scores = new int[this.parts.length]; + for (int i = 0; i < this.parts.length; i++) { + codes[i] = this.parts[i][0]; + scores[i] = skillFields[i].getValue(); + } + com.studentgui.apphelpers.Database.insertAssessmentResults(sessionId, ptId, codes, scores); + LOG.info("Data submitted successfully via normalized schema."); + com.studentgui.apphelpers.UiNotifier.show("Digital Literacy data saved."); + com.studentgui.apphelpers.dto.AssessmentPayload payload = new com.studentgui.apphelpers.dto.AssessmentPayload(sessionId, codes, scores); + java.nio.file.Path jsonOut = com.studentgui.apphelpers.SessionJsonWriter.writeSessionJson(this.studentNameParam, "DigitalLiteracy", payload, sessionId); + if (jsonOut == null) { + LOG.warn("Unable to save DigitalLiteracy session JSON for sessionId={}", sessionId); + } + try { + java.nio.file.Path plotsOut = com.studentgui.apphelpers.Helpers.studentPlotsDir(this.studentNameParam); + java.nio.file.Path reportsOut = com.studentgui.apphelpers.Helpers.studentReportsDir(this.studentNameParam); + java.nio.file.Files.createDirectories(plotsOut); + java.nio.file.Files.createDirectories(reportsOut); + java.time.format.DateTimeFormatter df = java.time.format.DateTimeFormatter.ISO_DATE; + String dateStr = this.dateParam != null ? this.dateParam.format(df) : java.time.LocalDate.now().toString(); + String baseName = "DigitalLiteracy-" + sessionId + "-" + dateStr; + + com.studentgui.apphelpers.Database.ResultsWithDates rwd = com.studentgui.apphelpers.Database.fetchLatestAssessmentResultsWithDates(this.studentNameParam, "DigitalLiteracy", Integer.MAX_VALUE); + java.util.Map groups = null; + String[] labels = new String[this.parts.length]; + for (int i = 0; i < this.parts.length; i++) { + labels[i] = this.parts[i][1]; + } + if (rwd != null && rwd.rows != null && !rwd.rows.isEmpty()) { + lineGraph.updateWithGroupedDataByDate(rwd.dates, rwd.rows, codes, labels); + groups = lineGraph.saveGroupedCharts(plotsOut, baseName, 1000, 240); + java.time.LocalDate headerDate = rwd.dates.get(rwd.dates.size() - 1); + dateStr = headerDate.format(df); + } else { + java.util.List> rowsList = new java.util.ArrayList<>(); + java.util.List latest = new java.util.ArrayList<>(); + for (int v : scores) latest.add(v); + rowsList.add(latest); + lineGraph.updateWithGroupedData(rowsList, codes); + groups = lineGraph.saveGroupedCharts(plotsOut, baseName, 1000, 240); + } + + if (groups == null) { + groups = new java.util.LinkedHashMap<>(); + } + StringBuilder md = new StringBuilder(); + md.append("# ").append(this.studentNameParam == null ? "Unknown Student" : this.studentNameParam).append(" - ").append(dateStr).append("\n\n"); + for (java.util.Map.Entry e : groups.entrySet()) { + md.append("## ").append(e.getKey()).append("\n\n"); + md.append("![](../plots/").append(e.getValue().getFileName().toString()).append(")\n\n"); + } + java.nio.file.Path mdFile = reportsOut.resolve(baseName + ".md"); + java.nio.file.Files.writeString(mdFile, md.toString(), java.nio.charset.StandardCharsets.UTF_8); + + try { + String[] palette = JLineGraph.PALETTE_HEX; + java.util.LinkedHashMap> groupsIdx = new java.util.LinkedHashMap<>(); + for (int i = 0; i < codes.length; i++) { + String code = codes[i]; + String grp = code != null && code.contains("_") ? code.split("_")[0] : code; + groupsIdx.computeIfAbsent(grp, k -> new java.util.ArrayList<>()).add(i); + } + StringBuilder html = new StringBuilder(); + html.append(""); + html.append(this.studentNameParam == null ? "Student Report" : this.studentNameParam).append(" - ").append(dateStr).append(""); + html.append(""); + html.append(""); + html.append("

").append(this.studentNameParam == null ? "Unknown Student" : this.studentNameParam).append(" - ").append(dateStr).append("

"); + for (java.util.Map.Entry e2 : groups.entrySet()) { + String grp = e2.getKey(); + String imgName = e2.getValue().getFileName().toString(); + html.append("

").append(grp).append("

"); + html.append("
\"").append(grp).append("\"
"); + java.util.List idxs = groupsIdx.getOrDefault(grp, new java.util.ArrayList<>()); + html.append("
"); + for (int s = 0; s < idxs.size(); s++) { + int idx = idxs.get(s); + String code = codes[idx]; + String human = this.parts[idx][1]; + String seriesName = code + " - " + human; + String color = palette[s % palette.length]; + html.append("
"); + html.append(""); + html.append("
"); + html.append(seriesName); + html.append("
"); + } + html.append("
"); + } + html.append(""); + java.nio.file.Path htmlFile = reportsOut.resolve(baseName + ".html"); + java.nio.file.Files.writeString(htmlFile, html.toString(), java.nio.charset.StandardCharsets.UTF_8); + LOG.info("Wrote DigitalLiteracy HTML session report {}", htmlFile); + } catch (java.io.IOException ioex) { + LOG.warn("Unable to write DigitalLiteracy HTML report: {}", ioex.toString()); + } + } catch (java.io.IOException ioe) { + LOG.warn("Unable to save DigitalLiteracy per-phase plots or markdown report: {}", ioe.toString()); + } + } catch (SQLException e) { + LOG.error("SQL error submitting Digital Literacy data", e); + JOptionPane.showMessageDialog(this, "Database error saving Digital Literacy data: " + e.getMessage(), "Database error", JOptionPane.ERROR_MESSAGE); + } + } + + /** + * Load recent assessment sessions and update the shared {@link JLineGraph} + * component with the returned values. + */ + private void refreshGraph() { + try { + List> allSkillValues = com.studentgui.apphelpers.Database.fetchLatestAssessmentResults(studentNameParam, "DigitalLiteracy", 5); + if (allSkillValues != null && !allSkillValues.isEmpty()) { + // Build canonical codes array in the same order used when ensuring parts + String[] codes = new String[this.parts.length]; + for (int i = 0; i < this.parts.length; i++) { + codes[i] = this.parts[i][0]; + } + lineGraph.updateWithGroupedData(allSkillValues, codes); + // Write to the consolidated per-run data dumps file when enabled + if (Boolean.parseBoolean(com.studentgui.apphelpers.Settings.get("dump.enabled", "false"))) { + try { + String appHome = System.getProperty("APP_HOME", com.studentgui.apphelpers.Helpers.APP_HOME.toString()); + String ts = System.getProperty("LOG_TS", String.valueOf(java.time.Instant.now().getEpochSecond())); + java.nio.file.Path logDir = java.nio.file.Paths.get(appHome).resolve("logs"); + java.nio.file.Files.createDirectories(logDir); + java.nio.file.Path logFile = logDir.resolve("data_dumps_" + ts + ".log"); + StringBuilder sb = new StringBuilder(); + sb.append("[DigitalLiteracy]").append(System.lineSeparator()); + sb.append(java.time.Instant.now().toString()).append(" - student=").append(this.studentNameParam).append(System.lineSeparator()); + sb.append("data=").append(allSkillValues.toString()).append(System.lineSeparator()); + sb.append(System.lineSeparator()); + java.nio.file.Files.writeString(logFile, sb.toString(), java.nio.charset.StandardCharsets.UTF_8, java.nio.file.StandardOpenOption.CREATE, java.nio.file.StandardOpenOption.APPEND); + } catch (java.io.IOException ioe) { + LOG.trace("Unable to write DigitalLiteracy load log: {}", ioe.toString()); + } + } + } else { + LOG.info("No data to plot."); + } + } catch (SQLException e) { + LOG.error("SQL error refreshing Digital Literacy graph", e); + } + } + + @Override + public void dateChanged(final LocalDate newDate) { + this.dateParam = newDate; + SwingUtilities.invokeLater(() -> { + refreshGraph(); + updateTitleDate(); + }); + } + + @Override + public void studentChanged(final String newStudent) { + this.studentNameParam = newStudent; + SwingUtilities.invokeLater(() -> { + refreshGraph(); + updateTitleDate(); + }); + } + + private void updateTitleDate() { + try { + String dateStr = this.dateParam != null ? this.dateParam.toString() : java.time.LocalDate.now().toString(); + this.titleLabel.setText(baseTitle + " - " + dateStr); + } catch (Exception ex) { + this.titleLabel.setText(baseTitle); + } + } + + +} diff --git a/src/main/java/com/studentgui/apppages/Homepage.java b/src/main/java/com/studentgui/apppages/Homepage.java index 322c07a..4f53396 100644 --- a/src/main/java/com/studentgui/apppages/Homepage.java +++ b/src/main/java/com/studentgui/apppages/Homepage.java @@ -1,75 +1,75 @@ -package com.studentgui.apppages; - -import java.awt.BorderLayout; - -import javax.swing.JLabel; -import javax.swing.JPanel; -import javax.swing.JScrollPane; -import javax.swing.JTextArea; -import javax.swing.SwingConstants; - -/** - * Simple homepage panel with application overview/help text. - * - *

Provides a small, static help and overview text area that can be - * embedded into the main application frame.

- */ -public class Homepage { - /** - * Create the homepage panel which contains a title and an overview/help - * text area. - * - * @return a ready-to-add {@link JPanel} containing the application overview - */ - public static JPanel create() { - JPanel p = new JPanel(new BorderLayout()); - JLabel title = new JLabel("Student Skills Progressions", SwingConstants.LEFT); - title.setFont(title.getFont().deriveFont(24f)); - title.getAccessibleContext().setAccessibleName("Student Skills Progressions title"); - title.setName("homepage_title"); - p.add(title, BorderLayout.NORTH); - - JTextArea body = new JTextArea(); - body.setLineWrap(true); - body.setWrapStyleWord(true); - String text = """ - Welcome to the Student Skills Progressions application. - - This tool helps educators track and record student progress across a set of vision and access skill areas (Braille, Abacus, Digital Literacy, iOS access, Screen Reader, CVI, Keyboarding, and more). - - How to use: - 1. Select a student from the Student dropdown at the top-left. - 2. Use the Date field to set the session date and click Apply to recreate pages for that date. - 3. Navigate to a skill page using the Navigate menu (or the top control bar). Each skill page contains standardized rows for entering phase/score values. - 4. Enter assessment data and notes on each page. Use the Save / Submit buttons on pages where available to persist data to the local SQLite database. - 5. The shared graph shows progress trends for the selected student. Session notes and contact logs provide a place for free-form observations and structured contact records. - - Data storage and export: - • All data is stored locally in a SQLite database under the application data folder. - • Use the Instructional Materials page to open and manage student-facing materials and reports. - - Support and workflow tips: - • Start each session by verifying the student and date, then move through skill pages, entering scores and notes. - • Use Contact Log to record family/guardian contact; structured fields make later reporting easier. - • If you need to reset or recreate pages for a student/date, use the Apply button after changing the date. - - Thanks for using the Student Skills Progressions application. - """; - body.setText(text); - body.setEditable(false); - body.setToolTipText("Overview and quick help about the application"); - body.getAccessibleContext().setAccessibleName("Homepage overview"); - JScrollPane bodyScroll = new JScrollPane(body); - bodyScroll.getAccessibleContext().setAccessibleName("Homepage overview scroll pane"); - body.setName("homepage_body"); - p.add(bodyScroll, BorderLayout.CENTER); - return p; - } - - /** - * Private constructor to prevent instantiation of this utility class. - */ - private Homepage() { - throw new AssertionError("Not instantiable"); - } -} +package com.studentgui.apppages; + +import java.awt.BorderLayout; + +import javax.swing.JLabel; +import javax.swing.JPanel; +import javax.swing.JScrollPane; +import javax.swing.JTextArea; +import javax.swing.SwingConstants; + +/** + * Simple homepage panel with application overview/help text. + * + *

Provides a small, static help and overview text area that can be + * embedded into the main application frame.

+ */ +public class Homepage { + /** + * Create the homepage panel which contains a title and an overview/help + * text area. + * + * @return a ready-to-add {@link JPanel} containing the application overview + */ + public static JPanel create() { + JPanel p = new JPanel(new BorderLayout()); + JLabel title = new JLabel("Student Skills Progressions", SwingConstants.LEFT); + title.setFont(title.getFont().deriveFont(24f)); + title.getAccessibleContext().setAccessibleName("Student Skills Progressions title"); + title.setName("homepage_title"); + p.add(title, BorderLayout.NORTH); + + JTextArea body = new JTextArea(); + body.setLineWrap(true); + body.setWrapStyleWord(true); + String text = """ + Welcome to the Student Skills Progressions application. + + This tool helps educators track and record student progress across a set of vision and access skill areas (Braille, Abacus, Digital Literacy, iOS access, Screen Reader, CVI, Keyboarding, and more). + + How to use: + 1. Select a student from the Student dropdown at the top-left. + 2. Use the Date field to set the session date and click Apply to recreate pages for that date. + 3. Navigate to a skill page using the Navigate menu (or the top control bar). Each skill page contains standardized rows for entering phase/score values. + 4. Enter assessment data and notes on each page. Use the Save / Submit buttons on pages where available to persist data to the local SQLite database. + 5. The shared graph shows progress trends for the selected student. Session notes and contact logs provide a place for free-form observations and structured contact records. + + Data storage and export: + • All data is stored locally in a SQLite database under the application data folder. + • Use the Instructional Materials page to open and manage student-facing materials and reports. + + Support and workflow tips: + • Start each session by verifying the student and date, then move through skill pages, entering scores and notes. + • Use Contact Log to record family/guardian contact; structured fields make later reporting easier. + • If you need to reset or recreate pages for a student/date, use the Apply button after changing the date. + + Thanks for using the Student Skills Progressions application. + """; + body.setText(text); + body.setEditable(false); + body.setToolTipText("Overview and quick help about the application"); + body.getAccessibleContext().setAccessibleName("Homepage overview"); + JScrollPane bodyScroll = new JScrollPane(body); + bodyScroll.getAccessibleContext().setAccessibleName("Homepage overview scroll pane"); + body.setName("homepage_body"); + p.add(bodyScroll, BorderLayout.CENTER); + return p; + } + + /** + * Private constructor to prevent instantiation of this utility class. + */ + private Homepage() { + throw new AssertionError("Not instantiable"); + } +} diff --git a/src/main/java/com/studentgui/apppages/IOS.java b/src/main/java/com/studentgui/apppages/IOS.java index e4b95fb..7391fa1 100644 --- a/src/main/java/com/studentgui/apppages/IOS.java +++ b/src/main/java/com/studentgui/apppages/IOS.java @@ -1,331 +1,395 @@ -package com.studentgui.apppages; - -import java.awt.BorderLayout; -import java.awt.Font; -import java.awt.GridBagConstraints; -import java.awt.GridBagLayout; -import java.awt.Insets; -import java.awt.event.ActionEvent; -import java.awt.event.KeyEvent; -import java.sql.SQLException; -import java.time.LocalDate; -import java.util.LinkedHashMap; -import java.util.Map; - -import javax.swing.JButton; -import javax.swing.JLabel; -import javax.swing.JPanel; -import javax.swing.JScrollPane; -import javax.swing.SwingUtilities; - -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; - -import com.studentgui.uicomp.PhaseScoreField; - -/** - * iOS / iPadOS skills progression page. - *

- * Presents a map of device and app related skills keyed by part codes and - * allows saving and plotting of recent assessment sessions using the shared - * {@link JLineGraph} instance. - *

- */ -public class IOS extends JPanel { - private static final Logger LOG = LoggerFactory.getLogger(IOS.class); - /** Mapping of iOS assessment part codes to their input components. */ - private final Map inputs = new LinkedHashMap<>(); - - /** Selected student display name used for saves and plots (may be null). */ - private final String studentNameParam; - - /** Session date to associate with saved iOS progress entries. */ - private final LocalDate dateParam; - - /** Shared graph component for plotting recent iOS assessment sessions. */ - private final JLineGraph graph; - - /** - * Construct the iOS page for the given student and date. - * - * @param studentName selected student name (may be null) - * @param date session date to associate with saved progress - * @param graph shared graph used to visualize recent sessions - */ - public IOS(String studentName, LocalDate date, JLineGraph graph) { - this.studentNameParam = (studentName == null || studentName.trim().isEmpty()) ? com.studentgui.apphelpers.Helpers.defaultStudent() : studentName; - this.dateParam = date; - this.graph = graph; - setLayout(new BorderLayout()); - - JPanel p = new JPanel(new GridBagLayout()); - JPanel view = new JPanel(new BorderLayout()); - view.add(p, BorderLayout.NORTH); - view.setBorder(javax.swing.BorderFactory.createEmptyBorder(20,20,20,20)); - JScrollPane scroll = new JScrollPane(view); - scroll.getAccessibleContext().setAccessibleName("iOS data entry scroll pane"); - GridBagConstraints gbc = new GridBagConstraints(); gbc.insets=new Insets(2,2,2,2); gbc.fill = GridBagConstraints.HORIZONTAL; gbc.anchor = GridBagConstraints.NORTHWEST; gbc.weightx = 1.0; - - JLabel title = new JLabel("iOS / iPad OS Skills"); - title.setFont(new Font(Font.SANS_SERIF, Font.BOLD, 16)); - title.getAccessibleContext().setAccessibleName("iOS Skills Title"); - title.setHorizontalAlignment(JLabel.LEFT); - gbc.gridx=0; gbc.gridy=0; gbc.gridwidth=2; p.add(title, gbc); - - String[][] parts = new String[][]{ - {"P1_1","1.1 Turn Device On/Off"},{"P1_2","1.2 Turn VoiceOver On/Off"},{"P1_3","1.3 Gestures to Click Icons"}, - {"P1_4","1.4 Home Screen Icons to Open Documents"},{"P1_5","1.5 Save Documents"},{"P1_6","1.6 Online Tools/Resources"}, - {"P1_7","1.7 Keyboarding"},{"P1_8","1.8 Use Different Elements"},{"P1_9","1.9 Control Center, App Switcher..."}, - {"P2_1","2.1 Write, edit save"},{"P2_2","2.2 Read, Navigate Document"},{"P2_3","2.3 Use Menubar"}, - {"P2_4","2.4 Highlight text, copy and paste text"},{"P2_5","2.5 Copy and paste images"},{"P2_6","2.6 Proofread and edit"}, - {"P3_1","3.1 Describe Spreadsheet"},{"P3_2","3.2 Explain terms and concepts"},{"P3_3","3.3 Enter/Edit data"}, - {"P3_4","3.4 Navigate Spreadsheet"},{"P3_5","3.5 Create Graphs"},{"P4_1","4.1 Create Presentation"}, - {"P4_2","4.2 Edit Slides"},{"P4_3","4.3 Add Images"},{"P4_4","4.4 Present Slides"},{"P4_5","4.5 Share Presentation"}, - {"P5_1","5.1 Acceptable Use Policy"},{"P5_2","5.2 Digital Citizenship"},{"P5_3","5.3 Online Safety"}, - {"P5_4","5.4 Copyright"},{"P5_5","5.5 Plagiarism"},{"P5_6","5.6 Privacy"},{"P5_7","5.7 Cyberbullying"}, - {"P6_1","6.1 Install Apps"},{"P6_2","6.2 Update Apps"},{"P6_3","6.3Delete Apps"},{"P6_4","6.4 Manage Storage"}, - {"P6_5","6.5 Accessibility Settings"},{"P6_6","6.6 Screen Time"},{"P6_7","6.7 Parental Controls"},{"P6_8","6.8 Bluetooth"}, - {"P6_9","6.9 Wi-Fi"},{"P6_10","6.10 AirDrop"},{"P6_11","6.11 Hotspot"} - }; - - java.awt.Font labelFont = new java.awt.Font(java.awt.Font.SANS_SERIF, java.awt.Font.PLAIN, 12); - String[] labels = java.util.Arrays.stream(parts).map(x->x[1]).toArray(String[]::new); - int maxPx = com.studentgui.uicomp.PhaseScoreField.computeMaxLabelPixelWidth(labelFont, labels); - com.studentgui.uicomp.PhaseScoreField.setGlobalLabelWidth(Math.min(360, Math.max(200, maxPx + 50))); - int row = 1; - for (String[] part : parts) { - gbc.gridx = 0; gbc.gridy = row; gbc.gridwidth = 2; - PhaseScoreField tf = new PhaseScoreField(part[1], 0); - tf.setToolTipText("Enter whole number score for " + part[1]); - tf.getAccessibleContext().setAccessibleName(part[1]); - tf.setName("ios_" + part[0]); - p.add(tf, gbc); - inputs.put(part[0], tf); - row++; - } - // Place Save and Open Latest side-by-side (Braille style) - gbc.gridx = 0; gbc.gridy = row; gbc.gridwidth = 1; gbc.anchor = GridBagConstraints.WEST; - JButton save = new JButton("Save iOS Data"); - save.setPreferredSize(new java.awt.Dimension(0, 32)); - save.addActionListener((ActionEvent e) -> { save(); plot(); }); - save.setToolTipText("Save iOS assessment for selected student"); - save.setMnemonic(KeyEvent.VK_S); - save.getAccessibleContext().setAccessibleName("Save iOS Data"); - p.add(save, gbc); - - gbc.gridx = 1; - JButton openLatest = new JButton("Open Latest Plot"); - openLatest.setPreferredSize(new java.awt.Dimension(0, 32)); - openLatest.addActionListener((ActionEvent e) -> { - java.nio.file.Path pth = com.studentgui.apphelpers.Helpers.latestPlotPath(this.studentNameParam, "iOS"); - if (pth == null) { - com.studentgui.apphelpers.UiNotifier.show("No iOS plot found for student"); - } else { - try { - java.awt.Desktop.getDesktop().open(pth.toFile()); - } catch (java.io.IOException | UnsupportedOperationException | SecurityException ex) { - com.studentgui.apphelpers.UiNotifier.show("Unable to open plot: " + pth.getFileName().toString()); - } - } - }); - p.add(openLatest, gbc); - - // consume remaining columns (if any) so layout stays compact and buttons are not clipped - gbc.gridx = 2; gbc.gridwidth = GridBagConstraints.REMAINDER; gbc.anchor = GridBagConstraints.WEST; - p.add(new JPanel(), gbc); - row++; - - add(scroll, BorderLayout.CENTER); - add(graph, BorderLayout.SOUTH); - - SwingUtilities.invokeLater(()->{ - view.setPreferredSize(view.getPreferredSize()); - scroll.getViewport().setViewPosition(new java.awt.Point(0,0)); - revalidate(); - }); - - SwingUtilities.invokeLater(() -> { - for (var f: inputs.values()) LOG.debug("IOS field {} labelWidth={} spinnerX={} gap={}", f.getLabel(), f.getLabelWrapWidth(), f.getSpinnerX(), f.getActualGap()); - }); - - com.studentgui.apphelpers.Helpers.createFolderHierarchy(); - initParts(); - } - - /** - * Ensure the iOS progress-type and part rows exist in the normalized - * database schema. - */ - private void initParts() { - try { - int ptId = com.studentgui.apphelpers.Database.getOrCreateProgressType("iOS"); - java.util.Set keys = inputs.keySet(); - String[] codes = new String[keys.size()]; - int idx = 0; - for (String k : keys) { - codes[idx++] = k; - } - com.studentgui.apphelpers.Database.ensureAssessmentParts(ptId, codes); - } catch (SQLException ex) { - LOG.error("Error ensuring iOS assessment parts", ex); - } - } - - /** - * Validate inputs and persist them as a new progress session for the - * selected student. - */ - private void save() { - if (this.studentNameParam == null || this.studentNameParam.trim().isEmpty()) { - javax.swing.JOptionPane.showMessageDialog(this, "Please select a student before saving iOS data.", "Missing student", javax.swing.JOptionPane.WARNING_MESSAGE); - return; - } - - try { - int studentId = com.studentgui.apphelpers.Database.getOrCreateStudent(this.studentNameParam); - int ptId = com.studentgui.apphelpers.Database.getOrCreateProgressType("iOS"); - int sessionId = com.studentgui.apphelpers.Database.createProgressSession(studentId, ptId, this.dateParam); - java.util.Set keys = inputs.keySet(); - String[] codes = new String[keys.size()]; int idx = 0; for (String k: keys) codes[idx++] = k; - int[] scores = new int[codes.length]; - for (int i = 0; i < codes.length; i++) { - scores[i] = inputs.get(codes[i]).getValue(); - } - com.studentgui.apphelpers.Database.insertAssessmentResults(sessionId, ptId, codes, scores); - LOG.info("iOS data saved for {}", this.studentNameParam); - com.studentgui.apphelpers.UiNotifier.show("iOS data saved."); - com.studentgui.apphelpers.dto.AssessmentPayload payload = new com.studentgui.apphelpers.dto.AssessmentPayload(sessionId, codes, scores); - java.nio.file.Path jsonOut = com.studentgui.apphelpers.SessionJsonWriter.writeSessionJson(this.studentNameParam, "iOS", payload, sessionId); - if (jsonOut == null) LOG.warn("Unable to save iOS session JSON for sessionId={}", sessionId); - try { - java.nio.file.Path plotsOut = com.studentgui.apphelpers.Helpers.studentPlotsDir(this.studentNameParam); - java.nio.file.Path reportsOut = com.studentgui.apphelpers.Helpers.studentReportsDir(this.studentNameParam); - java.nio.file.Files.createDirectories(plotsOut); - java.nio.file.Files.createDirectories(reportsOut); - java.time.format.DateTimeFormatter df = java.time.format.DateTimeFormatter.ISO_DATE; - String dateStr = this.dateParam != null ? this.dateParam.format(df) : java.time.LocalDate.now().toString(); - String baseName = "iOS-" + sessionId + "-" + dateStr; - - com.studentgui.apphelpers.Database.ResultsWithDates rwd = com.studentgui.apphelpers.Database.fetchLatestAssessmentResultsWithDates(this.studentNameParam, "iOS", Integer.MAX_VALUE); - java.util.Map groups = null; - String[] labels = new String[codes.length]; - for (int i = 0; i < codes.length; i++) { - labels[i] = inputs.get(codes[i]).getLabel(); - } - // codes already built above as 'codes' - if (rwd != null && rwd.rows != null && !rwd.rows.isEmpty()) { - graph.updateWithGroupedDataByDate(rwd.dates, rwd.rows, codes, labels); - groups = graph.saveGroupedCharts(plotsOut, baseName, 1000, 240); - java.time.LocalDate headerDate = rwd.dates.get(rwd.dates.size() - 1); - dateStr = headerDate.format(df); - } else { - java.util.List> rowsList = new java.util.ArrayList<>(); - java.util.List latest = new java.util.ArrayList<>(); - for (String c : codes) { - latest.add(inputs.get(c).getValue()); - } - rowsList.add(latest); - graph.updateWithGroupedData(rowsList, codes); - groups = graph.saveGroupedCharts(plotsOut, baseName, 1000, 240); - } - - if (groups == null) { - groups = new java.util.LinkedHashMap<>(); - } - StringBuilder md = new StringBuilder(); - md.append("# ").append(this.studentNameParam == null ? "Unknown Student" : this.studentNameParam).append(" - ").append(dateStr).append("\n\n"); - for (java.util.Map.Entry e : groups.entrySet()) { - md.append("## ").append(e.getKey()).append("\n\n"); - md.append("![](../plots/").append(e.getValue().getFileName().toString()).append(")\n\n"); - } - java.nio.file.Path mdFile = reportsOut.resolve(baseName + ".md"); - java.nio.file.Files.writeString(mdFile, md.toString(), java.nio.charset.StandardCharsets.UTF_8); - - try { - String[] palette = JLineGraph.PALETTE_HEX; - java.util.LinkedHashMap> groupsIdx = new java.util.LinkedHashMap<>(); - for (int i = 0; i < codes.length; i++) { - String code = codes[i]; - String grp = code != null && code.contains("_") ? code.split("_")[0] : code; - groupsIdx.computeIfAbsent(grp, k -> new java.util.ArrayList<>()).add(i); - } - StringBuilder html = new StringBuilder(); - html.append(""); - html.append(this.studentNameParam == null ? "Student Report" : this.studentNameParam).append(" - ").append(dateStr).append(""); - html.append(""); - html.append(""); - html.append("

").append(this.studentNameParam == null ? "Unknown Student" : this.studentNameParam).append(" - ").append(dateStr).append("

"); - for (java.util.Map.Entry e2 : groups.entrySet()) { - String grp = e2.getKey(); - String imgName = e2.getValue().getFileName().toString(); - html.append("

").append(grp).append("

"); - html.append("
\"").append(grp).append("\"
"); - java.util.List idxs = groupsIdx.getOrDefault(grp, new java.util.ArrayList<>()); - html.append("
"); - for (int s = 0; s < idxs.size(); s++) { - int itemIdx = idxs.get(s); - String code = codes[itemIdx]; - String human = labels[itemIdx]; - String seriesName = code + " - " + human; - String color = palette[s % palette.length]; - html.append("
"); - html.append(""); - html.append("
"); - html.append(seriesName); - html.append("
"); - } - html.append("
"); - } - html.append(""); - java.nio.file.Path htmlFile = reportsOut.resolve(baseName + ".html"); - java.nio.file.Files.writeString(htmlFile, html.toString(), java.nio.charset.StandardCharsets.UTF_8); - LOG.info("Wrote iOS HTML session report {}", htmlFile); - } catch (java.io.IOException ioex) { - LOG.warn("Unable to write iOS HTML report: {}", ioex.toString()); - } - } catch (java.io.IOException ioe) { - LOG.warn("Unable to save iOS per-phase plots or markdown report: {}", ioe.toString()); - } - } catch (SQLException ex) { - LOG.error("Error saving iOS data", ex); - javax.swing.JOptionPane.showMessageDialog(this, "Database error saving iOS data: " + ex.getMessage(), "Database error", javax.swing.JOptionPane.ERROR_MESSAGE); - } - } - - /** - * Fetch recent iOS assessment sessions and update the shared graph view. - */ - private void plot() { - LOG.info("Plot requested for {}", studentNameParam); - try { - java.util.List> data = com.studentgui.apphelpers.Database.fetchLatestAssessmentResults(this.studentNameParam, "iOS", 20); - if (data != null && !data.isEmpty()) { - // Build codes array in the same order as inputs were created - String[] codes = new String[inputs.size()]; - int idx = 0; for (String k: inputs.keySet()) codes[idx++] = k; - graph.updateWithGroupedData(data, codes); - // Save static PNG - if (this.studentNameParam != null && !this.studentNameParam.trim().isEmpty()) { - try { - java.nio.file.Path out = com.studentgui.apphelpers.Helpers.APP_HOME.resolve("StudentDataFiles").resolve(com.studentgui.apphelpers.Helpers.safeName(this.studentNameParam)).resolve("plots"); - java.nio.file.Files.createDirectories(out); - java.time.format.DateTimeFormatter df = java.time.format.DateTimeFormatter.ISO_DATE; - String dateStr = (this.dateParam != null ? this.dateParam.format(df) : java.time.LocalDate.now().toString()); - java.nio.file.Path file = out.resolve("iOS-" + dateStr + ".png"); - graph.saveChart(file, 800, 400); - LOG.info("Saved iOS plot to {}", file); - // Do not auto-open the plot here; only save it. Opening is handled - // by submit/save handlers or the Open Latest button. - com.studentgui.apphelpers.UiNotifier.show("iOS plot saved to " + file.toString()); - } catch (java.io.IOException ex) { LOG.warn("Unable to save iOS plot image: {}", ex.toString()); } - } - } else { - LOG.info("No iOS data to plot for {}", studentNameParam); - } - } catch (SQLException ex) { - LOG.error("Error fetching iOS data for plot", ex); - } - } -} +package com.studentgui.apppages; + +import java.awt.BorderLayout; +import java.awt.Font; +import java.awt.GridBagConstraints; +import java.awt.GridBagLayout; +import java.awt.Insets; +import java.awt.event.ActionEvent; +import java.awt.event.KeyEvent; +import java.sql.SQLException; +import java.time.LocalDate; +import java.util.LinkedHashMap; +import java.util.Map; + +import javax.swing.JButton; +import javax.swing.JLabel; +import javax.swing.JPanel; +import javax.swing.JScrollPane; +import javax.swing.SwingUtilities; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import com.studentgui.uicomp.PhaseScoreField; + +/** + * iOS and iPadOS assistive technology proficiency assessment page. + * + *

Provides structured evaluation of iOS/iPadOS device usage skills across + * 41 competencies organized into 6 functional domains:

+ * + *
    + *
  • Phase 1 (P1_1–P1_9): Device Basics and VoiceOver Fundamentals + *
      + *
    • Power management, VoiceOver activation/deactivation
    • + *
    • Core gestures (tap, swipe, rotor) for icon navigation and interaction
    • + *
    • Home screen management, document handling, keyboarding basics
    • + *
    • Control Center, App Switcher, and system-level navigation
    • + *
    + *
  • + *
  • Phase 2 (P2_1–P2_6): Word Processing and Document Creation + *
      + *
    • Creating, editing, and saving text documents
    • + *
    • Reading and navigating within documents using VoiceOver
    • + *
    • Menu bar interaction, text/image copy-paste workflows
    • + *
    • Proofreading and editing strategies with assistive technology
    • + *
    + *
  • + *
  • Phase 3 (P3_1–P3_5): Spreadsheet and Data Visualization + *
      + *
    • Spreadsheet concepts and terminology (rows, columns, cells, formulas)
    • + *
    • Data entry, editing, and spreadsheet navigation with VoiceOver
    • + *
    • Creating and interpreting charts/graphs from data
    • + *
    + *
  • + *
  • Phase 4 (P4_1–P4_5): Presentation Software + *
      + *
    • Creating and structuring presentations with accessible workflows
    • + *
    • Editing slides, adding multimedia content (images, audio)
    • + *
    • Presenting slides effectively using assistive technology
    • + *
    • Sharing and exporting presentations
    • + *
    + *
  • + *
  • Phase 5 (P5_1–P5_7): Digital Citizenship and Online Safety + *
      + *
    • Acceptable Use Policies, digital citizenship principles
    • + *
    • Online safety, privacy awareness, copyright/plagiarism concepts
    • + *
    • Recognizing and responding to cyberbullying
    • + *
    + *
  • + *
  • Phase 6 (P6_1–P6_11): Device Management and Connectivity + *
      + *
    • App installation, updates, deletion, storage management
    • + *
    • Accessibility settings configuration and customization
    • + *
    • Screen Time controls, Parental Controls
    • + *
    • Connectivity features: Bluetooth, Wi-Fi, AirDrop, Personal Hotspot
    • + *
    + *
  • + *
+ * + *

Data Management and Artifacts:

+ *
    + *
  • Scores captured via {@link PhaseScoreField} components (typically 0–4 integer range)
  • + *
  • Persisted to normalized schema using {@link com.studentgui.apphelpers.Database#insertAssessmentResults}
  • + *
  • JSON session export: {@code StudentDataFiles//Sessions/iOS/iOS--.json}
  • + *
  • Phase-grouped time-series PNG plots saved to {@code plots/} directory
  • + *
  • Markdown and HTML reports generated with embedded plots and color-coded legends
  • + *
+ * + *

The shared {@link JLineGraph} visualizes recent session trends grouped by phase prefix + * to maintain chart readability. This page operates on static student/date parameters and + * does not implement listener interfaces for dynamic updates.

+ * + * @see com.studentgui.apphelpers.Database + * @see JLineGraph + * @see PhaseScoreField + */ +public class IOS extends JPanel { + private static final Logger LOG = LoggerFactory.getLogger(IOS.class); + /** Mapping of iOS assessment part codes to their input components. */ + private final Map inputs = new LinkedHashMap<>(); + + /** Selected student display name used for saves and plots (may be null). */ + private final String studentNameParam; + + /** Session date to associate with saved iOS progress entries. */ + private final LocalDate dateParam; + + /** Shared graph component for plotting recent iOS assessment sessions. */ + private final JLineGraph graph; + + /** + * Construct the iOS page for the given student and date. + * + * @param studentName selected student name (may be null) + * @param date session date to associate with saved progress + * @param graph shared graph used to visualize recent sessions + */ + public IOS(String studentName, LocalDate date, JLineGraph graph) { + this.studentNameParam = (studentName == null || studentName.trim().isEmpty()) ? com.studentgui.apphelpers.Helpers.defaultStudent() : studentName; + this.dateParam = date; + this.graph = graph; + setLayout(new BorderLayout()); + + JPanel p = new JPanel(new GridBagLayout()); + JPanel view = new JPanel(new BorderLayout()); + view.add(p, BorderLayout.NORTH); + view.setBorder(javax.swing.BorderFactory.createEmptyBorder(20,20,20,20)); + JScrollPane scroll = new JScrollPane(view); + scroll.getAccessibleContext().setAccessibleName("iOS data entry scroll pane"); + GridBagConstraints gbc = new GridBagConstraints(); gbc.insets=new Insets(2,2,2,2); gbc.fill = GridBagConstraints.HORIZONTAL; gbc.anchor = GridBagConstraints.NORTHWEST; gbc.weightx = 1.0; + + JLabel title = new JLabel("iOS / iPad OS Skills"); + title.setFont(new Font(Font.SANS_SERIF, Font.BOLD, 16)); + title.getAccessibleContext().setAccessibleName("iOS Skills Title"); + title.setHorizontalAlignment(JLabel.LEFT); + gbc.gridx=0; gbc.gridy=0; gbc.gridwidth=2; p.add(title, gbc); + + String[][] parts = new String[][]{ + {"P1_1","1.1 Turn Device On/Off"},{"P1_2","1.2 Turn VoiceOver On/Off"},{"P1_3","1.3 Gestures to Click Icons"}, + {"P1_4","1.4 Home Screen Icons to Open Documents"},{"P1_5","1.5 Save Documents"},{"P1_6","1.6 Online Tools/Resources"}, + {"P1_7","1.7 Keyboarding"},{"P1_8","1.8 Use Different Elements"},{"P1_9","1.9 Control Center, App Switcher..."}, + {"P2_1","2.1 Write, edit save"},{"P2_2","2.2 Read, Navigate Document"},{"P2_3","2.3 Use Menubar"}, + {"P2_4","2.4 Highlight text, copy and paste text"},{"P2_5","2.5 Copy and paste images"},{"P2_6","2.6 Proofread and edit"}, + {"P3_1","3.1 Describe Spreadsheet"},{"P3_2","3.2 Explain terms and concepts"},{"P3_3","3.3 Enter/Edit data"}, + {"P3_4","3.4 Navigate Spreadsheet"},{"P3_5","3.5 Create Graphs"},{"P4_1","4.1 Create Presentation"}, + {"P4_2","4.2 Edit Slides"},{"P4_3","4.3 Add Images"},{"P4_4","4.4 Present Slides"},{"P4_5","4.5 Share Presentation"}, + {"P5_1","5.1 Acceptable Use Policy"},{"P5_2","5.2 Digital Citizenship"},{"P5_3","5.3 Online Safety"}, + {"P5_4","5.4 Copyright"},{"P5_5","5.5 Plagiarism"},{"P5_6","5.6 Privacy"},{"P5_7","5.7 Cyberbullying"}, + {"P6_1","6.1 Install Apps"},{"P6_2","6.2 Update Apps"},{"P6_3","6.3Delete Apps"},{"P6_4","6.4 Manage Storage"}, + {"P6_5","6.5 Accessibility Settings"},{"P6_6","6.6 Screen Time"},{"P6_7","6.7 Parental Controls"},{"P6_8","6.8 Bluetooth"}, + {"P6_9","6.9 Wi-Fi"},{"P6_10","6.10 AirDrop"},{"P6_11","6.11 Hotspot"} + }; + + java.awt.Font labelFont = new java.awt.Font(java.awt.Font.SANS_SERIF, java.awt.Font.PLAIN, 12); + String[] labels = java.util.Arrays.stream(parts).map(x->x[1]).toArray(String[]::new); + int maxPx = com.studentgui.uicomp.PhaseScoreField.computeMaxLabelPixelWidth(labelFont, labels); + com.studentgui.uicomp.PhaseScoreField.setGlobalLabelWidth(Math.min(360, Math.max(200, maxPx + 50))); + int row = 1; + for (String[] part : parts) { + gbc.gridx = 0; gbc.gridy = row; gbc.gridwidth = 2; + PhaseScoreField tf = new PhaseScoreField(part[1], 0); + tf.setToolTipText("Enter whole number score for " + part[1]); + tf.getAccessibleContext().setAccessibleName(part[1]); + tf.setName("ios_" + part[0]); + p.add(tf, gbc); + inputs.put(part[0], tf); + row++; + } + // Place Save and Open Latest side-by-side (Braille style) + gbc.gridx = 0; gbc.gridy = row; gbc.gridwidth = 1; gbc.anchor = GridBagConstraints.WEST; + JButton save = new JButton("Save iOS Data"); + save.setPreferredSize(new java.awt.Dimension(0, 32)); + save.addActionListener((ActionEvent e) -> { save(); plot(); }); + save.setToolTipText("Save iOS assessment for selected student"); + save.setMnemonic(KeyEvent.VK_S); + save.getAccessibleContext().setAccessibleName("Save iOS Data"); + p.add(save, gbc); + + gbc.gridx = 1; + JButton openLatest = new JButton("Open Latest Plot"); + openLatest.setPreferredSize(new java.awt.Dimension(0, 32)); + openLatest.addActionListener((ActionEvent e) -> { + java.nio.file.Path pth = com.studentgui.apphelpers.Helpers.latestPlotPath(this.studentNameParam, "iOS"); + if (pth == null) { + com.studentgui.apphelpers.UiNotifier.show("No iOS plot found for student"); + } else { + try { + java.awt.Desktop.getDesktop().open(pth.toFile()); + } catch (java.io.IOException | UnsupportedOperationException | SecurityException ex) { + com.studentgui.apphelpers.UiNotifier.show("Unable to open plot: " + pth.getFileName().toString()); + } + } + }); + p.add(openLatest, gbc); + + // consume remaining columns (if any) so layout stays compact and buttons are not clipped + gbc.gridx = 2; gbc.gridwidth = GridBagConstraints.REMAINDER; gbc.anchor = GridBagConstraints.WEST; + p.add(new JPanel(), gbc); + row++; + + add(scroll, BorderLayout.CENTER); + add(graph, BorderLayout.SOUTH); + + SwingUtilities.invokeLater(()->{ + view.setPreferredSize(view.getPreferredSize()); + scroll.getViewport().setViewPosition(new java.awt.Point(0,0)); + revalidate(); + }); + + SwingUtilities.invokeLater(() -> { + for (var f: inputs.values()) LOG.debug("IOS field {} labelWidth={} spinnerX={} gap={}", f.getLabel(), f.getLabelWrapWidth(), f.getSpinnerX(), f.getActualGap()); + }); + + com.studentgui.apphelpers.Helpers.createFolderHierarchy(); + initParts(); + } + + /** + * Ensure the iOS progress-type and part rows exist in the normalized + * database schema. + */ + private void initParts() { + try { + int ptId = com.studentgui.apphelpers.Database.getOrCreateProgressType("iOS"); + java.util.Set keys = inputs.keySet(); + String[] codes = new String[keys.size()]; + int idx = 0; + for (String k : keys) { + codes[idx++] = k; + } + com.studentgui.apphelpers.Database.ensureAssessmentParts(ptId, codes); + } catch (SQLException ex) { + LOG.error("Error ensuring iOS assessment parts", ex); + } + } + + /** + * Validate inputs and persist them as a new progress session for the + * selected student. + */ + private void save() { + if (this.studentNameParam == null || this.studentNameParam.trim().isEmpty()) { + javax.swing.JOptionPane.showMessageDialog(this, "Please select a student before saving iOS data.", "Missing student", javax.swing.JOptionPane.WARNING_MESSAGE); + return; + } + + try { + int studentId = com.studentgui.apphelpers.Database.getOrCreateStudent(this.studentNameParam); + int ptId = com.studentgui.apphelpers.Database.getOrCreateProgressType("iOS"); + int sessionId = com.studentgui.apphelpers.Database.createProgressSession(studentId, ptId, this.dateParam); + java.util.Set keys = inputs.keySet(); + String[] codes = new String[keys.size()]; int idx = 0; for (String k: keys) codes[idx++] = k; + int[] scores = new int[codes.length]; + for (int i = 0; i < codes.length; i++) { + scores[i] = inputs.get(codes[i]).getValue(); + } + com.studentgui.apphelpers.Database.insertAssessmentResults(sessionId, ptId, codes, scores); + LOG.info("iOS data saved for {}", this.studentNameParam); + com.studentgui.apphelpers.UiNotifier.show("iOS data saved."); + com.studentgui.apphelpers.dto.AssessmentPayload payload = new com.studentgui.apphelpers.dto.AssessmentPayload(sessionId, codes, scores); + java.nio.file.Path jsonOut = com.studentgui.apphelpers.SessionJsonWriter.writeSessionJson(this.studentNameParam, "iOS", payload, sessionId); + if (jsonOut == null) LOG.warn("Unable to save iOS session JSON for sessionId={}", sessionId); + try { + java.nio.file.Path plotsOut = com.studentgui.apphelpers.Helpers.studentPlotsDir(this.studentNameParam); + java.nio.file.Path reportsOut = com.studentgui.apphelpers.Helpers.studentReportsDir(this.studentNameParam); + java.nio.file.Files.createDirectories(plotsOut); + java.nio.file.Files.createDirectories(reportsOut); + java.time.format.DateTimeFormatter df = java.time.format.DateTimeFormatter.ISO_DATE; + String dateStr = this.dateParam != null ? this.dateParam.format(df) : java.time.LocalDate.now().toString(); + String baseName = "iOS-" + sessionId + "-" + dateStr; + + com.studentgui.apphelpers.Database.ResultsWithDates rwd = com.studentgui.apphelpers.Database.fetchLatestAssessmentResultsWithDates(this.studentNameParam, "iOS", Integer.MAX_VALUE); + java.util.Map groups = null; + String[] labels = new String[codes.length]; + for (int i = 0; i < codes.length; i++) { + labels[i] = inputs.get(codes[i]).getLabel(); + } + // codes already built above as 'codes' + if (rwd != null && rwd.rows != null && !rwd.rows.isEmpty()) { + graph.updateWithGroupedDataByDate(rwd.dates, rwd.rows, codes, labels); + groups = graph.saveGroupedCharts(plotsOut, baseName, 1000, 240); + java.time.LocalDate headerDate = rwd.dates.get(rwd.dates.size() - 1); + dateStr = headerDate.format(df); + } else { + java.util.List> rowsList = new java.util.ArrayList<>(); + java.util.List latest = new java.util.ArrayList<>(); + for (String c : codes) { + latest.add(inputs.get(c).getValue()); + } + rowsList.add(latest); + graph.updateWithGroupedData(rowsList, codes); + groups = graph.saveGroupedCharts(plotsOut, baseName, 1000, 240); + } + + if (groups == null) { + groups = new java.util.LinkedHashMap<>(); + } + StringBuilder md = new StringBuilder(); + md.append("# ").append(this.studentNameParam == null ? "Unknown Student" : this.studentNameParam).append(" - ").append(dateStr).append("\n\n"); + for (java.util.Map.Entry e : groups.entrySet()) { + md.append("## ").append(e.getKey()).append("\n\n"); + md.append("![](../plots/").append(e.getValue().getFileName().toString()).append(")\n\n"); + } + java.nio.file.Path mdFile = reportsOut.resolve(baseName + ".md"); + java.nio.file.Files.writeString(mdFile, md.toString(), java.nio.charset.StandardCharsets.UTF_8); + + try { + String[] palette = JLineGraph.PALETTE_HEX; + java.util.LinkedHashMap> groupsIdx = new java.util.LinkedHashMap<>(); + for (int i = 0; i < codes.length; i++) { + String code = codes[i]; + String grp = code != null && code.contains("_") ? code.split("_")[0] : code; + groupsIdx.computeIfAbsent(grp, k -> new java.util.ArrayList<>()).add(i); + } + StringBuilder html = new StringBuilder(); + html.append(""); + html.append(this.studentNameParam == null ? "Student Report" : this.studentNameParam).append(" - ").append(dateStr).append(""); + html.append(""); + html.append(""); + html.append("

").append(this.studentNameParam == null ? "Unknown Student" : this.studentNameParam).append(" - ").append(dateStr).append("

"); + for (java.util.Map.Entry e2 : groups.entrySet()) { + String grp = e2.getKey(); + String imgName = e2.getValue().getFileName().toString(); + html.append("

").append(grp).append("

"); + html.append("
\"").append(grp).append("\"
"); + java.util.List idxs = groupsIdx.getOrDefault(grp, new java.util.ArrayList<>()); + html.append("
"); + for (int s = 0; s < idxs.size(); s++) { + int itemIdx = idxs.get(s); + String code = codes[itemIdx]; + String human = labels[itemIdx]; + String seriesName = code + " - " + human; + String color = palette[s % palette.length]; + html.append("
"); + html.append(""); + html.append("
"); + html.append(seriesName); + html.append("
"); + } + html.append("
"); + } + html.append(""); + java.nio.file.Path htmlFile = reportsOut.resolve(baseName + ".html"); + java.nio.file.Files.writeString(htmlFile, html.toString(), java.nio.charset.StandardCharsets.UTF_8); + LOG.info("Wrote iOS HTML session report {}", htmlFile); + } catch (java.io.IOException ioex) { + LOG.warn("Unable to write iOS HTML report: {}", ioex.toString()); + } + } catch (java.io.IOException ioe) { + LOG.warn("Unable to save iOS per-phase plots or markdown report: {}", ioe.toString()); + } + } catch (SQLException ex) { + LOG.error("Error saving iOS data", ex); + javax.swing.JOptionPane.showMessageDialog(this, "Database error saving iOS data: " + ex.getMessage(), "Database error", javax.swing.JOptionPane.ERROR_MESSAGE); + } + } + + /** + * Fetch recent iOS assessment sessions and update the shared graph view. + */ + private void plot() { + LOG.info("Plot requested for {}", studentNameParam); + try { + java.util.List> data = com.studentgui.apphelpers.Database.fetchLatestAssessmentResults(this.studentNameParam, "iOS", 20); + if (data != null && !data.isEmpty()) { + // Build codes array in the same order as inputs were created + String[] codes = new String[inputs.size()]; + int idx = 0; for (String k: inputs.keySet()) codes[idx++] = k; + graph.updateWithGroupedData(data, codes); + // Save static PNG + if (this.studentNameParam != null && !this.studentNameParam.trim().isEmpty()) { + try { + java.nio.file.Path out = com.studentgui.apphelpers.Helpers.APP_HOME.resolve("StudentDataFiles").resolve(com.studentgui.apphelpers.Helpers.safeName(this.studentNameParam)).resolve("plots"); + java.nio.file.Files.createDirectories(out); + java.time.format.DateTimeFormatter df = java.time.format.DateTimeFormatter.ISO_DATE; + String dateStr = (this.dateParam != null ? this.dateParam.format(df) : java.time.LocalDate.now().toString()); + java.nio.file.Path file = out.resolve("iOS-" + dateStr + ".png"); + graph.saveChart(file, 800, 400); + LOG.info("Saved iOS plot to {}", file); + // Do not auto-open the plot here; only save it. Opening is handled + // by submit/save handlers or the Open Latest button. + com.studentgui.apphelpers.UiNotifier.show("iOS plot saved to " + file.toString()); + } catch (java.io.IOException ex) { LOG.warn("Unable to save iOS plot image: {}", ex.toString()); } + } + } else { + LOG.info("No iOS data to plot for {}", studentNameParam); + } + } catch (SQLException ex) { + LOG.error("Error fetching iOS data for plot", ex); + } + } +} diff --git a/src/main/java/com/studentgui/apppages/InstructionalMaterials.java b/src/main/java/com/studentgui/apppages/InstructionalMaterials.java index aaab6e1..4e0d8e4 100644 --- a/src/main/java/com/studentgui/apppages/InstructionalMaterials.java +++ b/src/main/java/com/studentgui/apppages/InstructionalMaterials.java @@ -1,57 +1,77 @@ -package com.studentgui.apppages; - -import java.awt.BorderLayout; -import java.awt.Font; -import java.awt.GridBagConstraints; -import java.awt.GridBagLayout; -import java.awt.Insets; -import java.awt.event.ActionEvent; -import java.awt.event.KeyEvent; - -import javax.swing.JButton; -import javax.swing.JLabel; -import javax.swing.JPanel; -import javax.swing.JScrollPane; -import javax.swing.JTextArea; -import javax.swing.SwingUtilities; - -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; - -/** - * Instructional materials viewer panel. - *

- * Displays a read-only listing or links to instructional resources. This is - * primarily a static help/documentation view and does not persist data. - *

- */ -public class InstructionalMaterials extends JPanel { - private static final Logger LOG = LoggerFactory.getLogger(InstructionalMaterials.class); - - /** - * Create the Instructional Materials page. - */ - public InstructionalMaterials() { - setLayout(new BorderLayout()); - JPanel p = new JPanel(new GridBagLayout()); - JPanel view = new JPanel(new BorderLayout()); - view.add(p, BorderLayout.NORTH); - view.setBorder(javax.swing.BorderFactory.createEmptyBorder(20,20,20,20)); - JScrollPane scroll = new JScrollPane(view); - scroll.getAccessibleContext().setAccessibleName("Instructional Materials scroll pane"); - GridBagConstraints gbc = new GridBagConstraints(); gbc.insets=new Insets(2,2,2,2); gbc.fill=GridBagConstraints.BOTH; - JLabel title = new JLabel("Instructional Materials", JLabel.LEFT); - title.setFont(title.getFont().deriveFont(Font.BOLD,16)); - title.getAccessibleContext().setAccessibleName("Instructional Materials Title"); - gbc.gridx=0; gbc.gridy=0; p.add(title, gbc); - - int globalLabel = com.studentgui.uicomp.PhaseScoreField.getGlobalLabelWidth(); - JLabel areaLabel = new JLabel("Materials:"); areaLabel.setPreferredSize(new java.awt.Dimension(globalLabel, areaLabel.getPreferredSize().height)); gbc.gridy=1; p.add(areaLabel, gbc); - JTextArea area = new JTextArea(20,60); area.setEditable(false); area.setText("Instructional materials listing placeholder. Add docs or links here."); area.setToolTipText("Instructional materials and links"); area.getAccessibleContext().setAccessibleName("Instructional materials"); gbc.gridy=2; p.add(area, gbc); - areaLabel.setLabelFor(area); - JButton refresh = new JButton("Refresh"); refresh.addActionListener((ActionEvent e)-> LOG.info("Refresh requested")); refresh.setToolTipText("Refresh the instructional materials listing"); refresh.setMnemonic(KeyEvent.VK_R); refresh.getAccessibleContext().setAccessibleName("Refresh instructional materials"); gbc.gridy=3; p.add(refresh, gbc); - - add(scroll, BorderLayout.CENTER); - SwingUtilities.invokeLater(()->{ p.setPreferredSize(p.getPreferredSize()); revalidate(); }); - } -} +package com.studentgui.apppages; + +import java.awt.BorderLayout; +import java.awt.Font; +import java.awt.GridBagConstraints; +import java.awt.GridBagLayout; +import java.awt.Insets; +import java.awt.event.ActionEvent; +import java.awt.event.KeyEvent; + +import javax.swing.JButton; +import javax.swing.JLabel; +import javax.swing.JPanel; +import javax.swing.JScrollPane; +import javax.swing.JTextArea; +import javax.swing.SwingUtilities; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + * Instructional materials and resources reference page. + * + *

Provides a simple placeholder panel for displaying links, documentation, or references + * to external instructional resources. This is a static informational view without data + * persistence or assessment functionality.

+ * + *

Current Implementation:

+ *
    + *
  • Read-only text area with placeholder content
  • + *
  • Refresh button (currently logs action but performs no operation)
  • + *
  • No database persistence or session tracking
  • + *
  • Intended for future expansion with resource links, PDF viewers, or material management UI
  • + *
+ * + *

Potential Future Enhancements:

+ *
    + *
  • Dynamic listing of student-specific materials from {@code StudentDataFiles//InstructionalMaterials/}
  • + *
  • PDF preview integration for viewing documents inline
  • + *
  • File upload and organization capabilities
  • + *
  • Links to online resources (curriculum guides, training videos, vendor documentation)
  • + *
  • Material assignment workflow (track which materials were provided to student/family)
  • + *
+ * + *

This page does not implement listener interfaces and does not interact with the database. + * It serves as a navigation target and placeholder for future resource management features.

+ */ +public class InstructionalMaterials extends JPanel { + private static final Logger LOG = LoggerFactory.getLogger(InstructionalMaterials.class); + + /** + * Create the Instructional Materials page. + */ + public InstructionalMaterials() { + setLayout(new BorderLayout()); + JPanel p = new JPanel(new GridBagLayout()); + JPanel view = new JPanel(new BorderLayout()); + view.add(p, BorderLayout.NORTH); + view.setBorder(javax.swing.BorderFactory.createEmptyBorder(20,20,20,20)); + JScrollPane scroll = new JScrollPane(view); + scroll.getAccessibleContext().setAccessibleName("Instructional Materials scroll pane"); + GridBagConstraints gbc = new GridBagConstraints(); gbc.insets=new Insets(2,2,2,2); gbc.fill=GridBagConstraints.BOTH; + JLabel title = new JLabel("Instructional Materials", JLabel.LEFT); + title.setFont(title.getFont().deriveFont(Font.BOLD,16)); + title.getAccessibleContext().setAccessibleName("Instructional Materials Title"); + gbc.gridx=0; gbc.gridy=0; p.add(title, gbc); + + int globalLabel = com.studentgui.uicomp.PhaseScoreField.getGlobalLabelWidth(); + JLabel areaLabel = new JLabel("Materials:"); areaLabel.setPreferredSize(new java.awt.Dimension(globalLabel, areaLabel.getPreferredSize().height)); gbc.gridy=1; p.add(areaLabel, gbc); + JTextArea area = new JTextArea(20,60); area.setEditable(false); area.setText("Instructional materials listing placeholder. Add docs or links here."); area.setToolTipText("Instructional materials and links"); area.getAccessibleContext().setAccessibleName("Instructional materials"); gbc.gridy=2; p.add(area, gbc); + areaLabel.setLabelFor(area); + JButton refresh = new JButton("Refresh"); refresh.addActionListener((ActionEvent e)-> LOG.info("Refresh requested")); refresh.setToolTipText("Refresh the instructional materials listing"); refresh.setMnemonic(KeyEvent.VK_R); refresh.getAccessibleContext().setAccessibleName("Refresh instructional materials"); gbc.gridy=3; p.add(refresh, gbc); + + add(scroll, BorderLayout.CENTER); + SwingUtilities.invokeLater(()->{ p.setPreferredSize(p.getPreferredSize()); revalidate(); }); + } +} diff --git a/src/main/java/com/studentgui/apppages/JLineGraph.java b/src/main/java/com/studentgui/apppages/JLineGraph.java index 8bf6bc3..8bc65df 100644 --- a/src/main/java/com/studentgui/apppages/JLineGraph.java +++ b/src/main/java/com/studentgui/apppages/JLineGraph.java @@ -1,797 +1,846 @@ -package com.studentgui.apppages; - -import java.awt.BasicStroke; -import java.awt.BorderLayout; -import java.awt.Color; -import java.awt.Dimension; -import java.awt.Font; -import java.util.List; -import java.util.Random; -import java.util.concurrent.ThreadLocalRandom; - -import javax.swing.JPanel; - -import org.jfree.chart.ChartFactory; -import org.jfree.chart.ChartPanel; -import org.jfree.chart.JFreeChart; -import org.jfree.chart.annotations.XYPolygonAnnotation; -import org.jfree.chart.axis.NumberAxis; -import org.jfree.chart.plot.PlotOrientation; -import org.jfree.chart.plot.XYPlot; -import org.jfree.chart.renderer.xy.XYLineAndShapeRenderer; -import org.jfree.data.xy.XYSeries; -import org.jfree.data.xy.XYSeriesCollection; - -/** - * Lightweight line chart component used across pages to display recent - * assessment sessions. Wraps a JFreeChart XY plot and exposes a small set - * of convenience update methods used by the application pages: - *
    - *
  • {@link #updateWithData(java.util.List)}
  • - *
  • {@link #updateWithGroupedData(java.util.List, String[])}
  • - *
  • {@link #updateWithGroupedDataByDate(java.util.List, java.util.List, String[], String[])}
  • - *
- * - * Important implementation notes: - *
    - *
  • Rendering jitter: a small visual jitter of +/- {@code JITTER_AMPLITUDE} - * is applied to plotted points via {@link #addJitter(double)} to help - * reveal overlapping points. This is a display-only transformation - * and does not modify persisted session values.
  • - *
  • Background bands: the component draws horizontal colored bands to - * indicate score ranges; the bands use the ranges: red = -0.25..0.5, - * orange = 0.5..1.5, orange = 1.5..2.5, yellow = 2.5..3.5, green = - * 3.5..4.5. The Y-axis range is set to {@code -0.25 .. 4.25} by default.
  • - *
  • Grouped charts and time-series charts share the same band drawing - * helper {@link #addHorizontalBands(org.jfree.chart.plot.XYPlot, double, double)}
  • - *
- */ -public class JLineGraph extends JPanel implements com.studentgui.app.SettingsChangeListener { - private static final long serialVersionUID = 1L; - private static final org.slf4j.Logger LOG = org.slf4j.LoggerFactory.getLogger(JLineGraph.class); - /** The dataset containing XY series for historical and latest sessions. */ - private final XYSeriesCollection lineDataset; - /** The JFreeChart instance used to render the plot. */ - private final JFreeChart chart; - /** Panel that embeds the chart and provides UI features. */ - private final ChartPanel chartPanel; - /** When rendering grouped charts we place multiple ChartPanels in this container. */ - private javax.swing.JPanel multiChartContainer; - /** Domain axis used to customise X-axis labels and range. */ - private final NumberAxis xAxis; - /** Expected number of skill columns per session. */ - private static final int NUMBER_OF_SKILLS = 28; // Adjust as needed - /** Jitter amplitude (plus/minus) applied to plotted data points. */ - private static final double JITTER_AMPLITUDE = 0.10d; - - /** Whether rendering jitter is currently enabled. Default: true. */ - private boolean jitterEnabled = true; - /** When true, use a deterministic java.util.Random seeded RNG instead of ThreadLocalRandom. */ - private boolean jitterDeterministic = false; - /** Optional seed used when deterministic jitter is enabled. */ - private Long jitterSeed = null; - /** Cached Random instance when deterministic mode is enabled. */ - private Random deterministicRandom = null; - - /** - * Add a small random jitter within +/- JITTER_AMPLITUDE to the provided value. - * When jitter is disabled this returns the original value unchanged. - */ - private double addJitter(final double v) { - if (!jitterEnabled) { - return v; - } - try { - if (jitterDeterministic) { - if (deterministicRandom == null) { - long seed = jitterSeed == null ? 0L : jitterSeed.longValue(); - deterministicRandom = new Random(seed); - } - double r = deterministicRandom.nextDouble() * 2.0 - 1.0; // -1..1 - return v + (r * JITTER_AMPLITUDE); - } else { - return v + ThreadLocalRandom.current().nextDouble(-JITTER_AMPLITUDE, JITTER_AMPLITUDE); - } - } catch (Throwable t) { - // In the unlikely event RNG is unavailable, fall back to no jitter - return v; - } - } - /** Public color palette (hex) for HTML legends and consistency across pages. */ - public static final String[] PALETTE_HEX = new String[] { - "#1b9e77","#d95f02","#7570b3","#e7298a","#66a61e","#e6ab02","#a6761d","#666666" - }; - /** Public color palette as AWT Color objects for chart rendering. */ - public static final java.awt.Color[] PALETTE = new java.awt.Color[] { - new java.awt.Color(0x1b9e77), - new java.awt.Color(0xd95f02), - new java.awt.Color(0x7570b3), - new java.awt.Color(0xe7298a), - new java.awt.Color(0x66a61e), - new java.awt.Color(0xe6ab02), - new java.awt.Color(0xa6761d), - new java.awt.Color(0x666666) - }; - - /** - * Create a new JLineGraph with default styling and an empty dataset. - */ - public JLineGraph() { - setLayout(new BorderLayout()); - lineDataset = new XYSeriesCollection(); - - // Create a chart - chart = ChartFactory.createXYLineChart( - "Skill Progression", - "Skills", - "Value", - lineDataset, - PlotOrientation.VERTICAL, - true, - true, - false - ); - - // Customize the plot - XYPlot plot = chart.getXYPlot(); - plot.setBackgroundPaint(Color.WHITE); - plot.setDomainGridlinePaint(Color.GRAY); - plot.setRangeGridlinePaint(Color.GRAY); - - // Set axis ranges - xAxis = (NumberAxis) plot.getDomainAxis(); - xAxis.setRange(0, NUMBER_OF_SKILLS + 1); - NumberAxis yAxis = (NumberAxis) plot.getRangeAxis(); - yAxis.setRange(-0.25, 4.25); - - // Create background bands - addBackgroundBands(plot); - - chartPanel = new ChartPanel(chart); - chartPanel.setPreferredSize(new Dimension(800, 600)); - chartPanel.getAccessibleContext().setAccessibleName("Skill progression chart"); - chartPanel.setToolTipText("Skill progression chart showing historical and latest values"); - add(chartPanel, BorderLayout.CENTER); - multiChartContainer = null; - - // Set custom X-axis labels - updateXAxisLabels(); - // Apply any persisted settings at creation time - try { - settingsChanged(); - } catch (Throwable t) { - // ignore any issues reading settings at startup - } - } - - @Override - public void settingsChanged() { - try { - String je = com.studentgui.apphelpers.Settings.get("jitter.enabled", String.valueOf(this.jitterEnabled)); - setJitterEnabled("true".equalsIgnoreCase(je)); - String jd = com.studentgui.apphelpers.Settings.get("jitter.deterministic", String.valueOf(this.jitterDeterministic)); - setJitterDeterministic("true".equalsIgnoreCase(jd)); - String s = com.studentgui.apphelpers.Settings.get("jitter.seed", this.jitterSeed == null ? "" : String.valueOf(this.jitterSeed)); - if (s == null || s.trim().isEmpty()) { - setJitterSeed(null); - } else { - try { - long v = Long.parseLong(s.trim()); - setJitterSeed(Long.valueOf(v)); - } catch (NumberFormatException nfe) { - setJitterSeed(null); - } - } - // reset cached RNG so seed/cfg takes effect - this.deterministicRandom = null; - if (chart != null) { - chart.fireChartChanged(); - } - if (chartPanel != null) { - chartPanel.repaint(); - } - } catch (Throwable t) { - LOG.debug("Failed applying settings: {}", t.toString()); - } - } - - /** - * Add lightly-colored horizontal bands to the plot to indicate score - * ranges. - */ - private void addBackgroundBands(final XYPlot plot) { - // Use the generic band painter to draw the requested bands across the - // full X domain of the main chart. - double left = 0.0; - double right = NUMBER_OF_SKILLS + 1; - addHorizontalBands(plot, left, right); - } - - /** - * Add horizontal background bands to the provided plot between left and right - * X coordinates. Bands follow the requested ranges: - * red = -0.25..0.5, orange = 0.5..1.5, orange = 1.5..2.5, yellow = 2.5..3.5, - * green = 3.5..4.5 - */ - private void addHorizontalBands(final XYPlot plot, final double left, final double right) { - try { - java.awt.Color red = new java.awt.Color(255, 0, 0, 40); - java.awt.Color orange = new java.awt.Color(255, 165, 0, 40); - java.awt.Color orange2 = new java.awt.Color(255, 140, 0, 40); - java.awt.Color yellow = new java.awt.Color(255, 255, 0, 40); - java.awt.Color green = new java.awt.Color(0, 255, 0, 40); - - double[][] bands = new double[][]{ - { -0.25, 0.5 }, - { 0.5, 1.5 }, - { 1.5, 2.5 }, - { 2.5, 3.5 }, - { 3.5, 4.5 } - }; - java.awt.Color[] colors = new java.awt.Color[] { red, orange, orange2, yellow, green }; - - for (int i = 0; i < bands.length; i++) { - double low = bands[i][0]; - double high = bands[i][1]; - double[] coords = new double[] { left, low, right, low, right, high, left, high }; - plot.addAnnotation(new XYPolygonAnnotation(coords, null, null, colors[i])); - } - } catch (Throwable t) { - LOG.debug("Unable to add horizontal bands: {}", t.toString()); - } - } - - /** - * Enable or disable rendering jitter at runtime. - * @param enabled true to enable jitter, false to draw raw values - */ - public void setJitterEnabled(final boolean enabled) { - this.jitterEnabled = enabled; - } - - /** - * Query whether rendering jitter is currently enabled. - * - * @return true when jitter is enabled, false otherwise - */ - public boolean isJitterEnabled() { - return this.jitterEnabled; - } - - /** - * Enable/disable deterministic (seeded) jitter. - * When enabled, jitter will be generated from a java.util.Random seeded - * with {@link #jitterSeed} (or 0 when seed is null). - * - * @param deterministic true to use a seeded RNG, false to use non-deterministic RNG - */ - public void setJitterDeterministic(final boolean deterministic) { - this.jitterDeterministic = deterministic; - this.deterministicRandom = null; // reset instance so seed takes effect - } - - /** - * Query whether deterministic jitter is enabled. - * - * @return true when deterministic (seeded) jitter is enabled - */ - public boolean isJitterDeterministic() { - return this.jitterDeterministic; - } - - /** - * Set the seed used when deterministic jitter is enabled. Pass null to - * clear the seed (will use 0 when a deterministic RNG is created). - * - * @param seed seed value or null to clear - */ - public void setJitterSeed(final Long seed) { - this.jitterSeed = seed; - this.deterministicRandom = null; - } - - /** - * Return the currently configured jitter seed or null when unset. - * - * @return configured seed value or null when not set - */ - public Long getJitterSeed() { - return this.jitterSeed; - } - - /** - * Replace the current dataset with the provided list of skill value - * series. Each inner list represents a single session and must contain - * NUMBER_OF_SKILLS entries. - * - * @param allSkillValues list of sessions where each session is a list of - * integer skill values (older sessions first) - */ - public void updateWithData(final List> allSkillValues) { - LOG.debug("updateWithData called with {} rows", allSkillValues == null ? 0 : allSkillValues.size()); - if (allSkillValues == null || allSkillValues.isEmpty()) { - return; - } - // Fallback to existing single-chart behavior - lineDataset.removeAllSeries(); - XYLineAndShapeRenderer renderer = new XYLineAndShapeRenderer(); - - // Add historical data series (each prior session as a separate series) - for (int s = 0; s < allSkillValues.size() - 1; s++) { - XYSeries hs = new XYSeries("S" + s); - List skillValues = allSkillValues.get(s); - if (skillValues == null) { - continue; - } - for (int j = 0; j < skillValues.size(); j++) { - Integer v = skillValues.get(j); - double y = (double) (v == null ? 0 : v); - hs.add(j + 1, addJitter(y)); - } - lineDataset.addSeries(hs); - renderer.setSeriesPaint(s, Color.GRAY); - renderer.setSeriesStroke(s, new BasicStroke(2.0f)); - renderer.setSeriesShapesVisible(s, false); - } - - // Latest session - XYSeries latestSeries = new XYSeries("Latest"); - List latestSkillValues = allSkillValues.get(allSkillValues.size() - 1); - if (latestSkillValues != null) { - for (int i = 0; i < latestSkillValues.size(); i++) { - Integer v = latestSkillValues.get(i); - double y = (double) (v == null ? 0 : v); - latestSeries.add(i + 1, addJitter(y)); - } - } - lineDataset.addSeries(latestSeries); - int latestIndex = lineDataset.getSeriesCount() - 1; - renderer.setSeriesPaint(latestIndex, Color.BLACK); - renderer.setSeriesStroke(latestIndex, new BasicStroke(3f)); - renderer.setSeriesShapesVisible(latestIndex, true); - renderer.setSeriesShape(latestIndex, new java.awt.geom.Ellipse2D.Double(-6, -6, 12, 12)); - - chart.getXYPlot().setDataset(lineDataset); - chart.getXYPlot().setRenderer(renderer); - // Ensure Y axis range and ticks are consistent across charts - try { - NumberAxis y = (NumberAxis) chart.getXYPlot().getRangeAxis(); - y.setRange(-0.25, 4.25); - y.setTickUnit(new org.jfree.chart.axis.NumberTickUnit(1)); - } catch (ClassCastException ignored) { - // if range axis isn't a NumberAxis, ignore - } - chart.fireChartChanged(); - chartPanel.repaint(); - } - - /** - * Update the component with grouped plots. Each group is determined by the - * prefix of the part code (e.g. 'P1' from 'P1_1'). For each group we render - * a separate small chart stacked vertically. - * - * @param allSkillValues list of sessions (older first) where each session is a list of integer skill values - * @param partCodes array of part codes aligned with columns in each session row - */ - public void updateWithGroupedData(final List> allSkillValues, final String[] partCodes) { - LOG.debug("updateWithGroupedData called with rows={} partCodes={}", allSkillValues == null ? 0 : allSkillValues.size(), partCodes == null ? 0 : partCodes.length); - // validate - if (partCodes == null || partCodes.length == 0 || allSkillValues == null || allSkillValues.isEmpty()) { - return; - } - - // Build group -> indexes map preserving order of first occurrence - java.util.LinkedHashMap> groups = new java.util.LinkedHashMap<>(); - for (int i = 0; i < partCodes.length; i++) { - String code = partCodes[i]; - String grp = code != null && code.contains("_") ? code.split("_")[0] : code; - groups.computeIfAbsent(grp, k -> new java.util.ArrayList<>()).add(i); - } - - // Remove any single chart mode UI - removeAll(); - multiChartContainer = new javax.swing.JPanel(); - multiChartContainer.setLayout(new javax.swing.BoxLayout(multiChartContainer, javax.swing.BoxLayout.Y_AXIS)); - - // For each group create a small chart - for (var entry : groups.entrySet()) { - String grp = entry.getKey(); - java.util.List idxs = entry.getValue(); - XYSeriesCollection dataset = new XYSeriesCollection(); - // historical sessions: create one series per prior session - int sessions = allSkillValues.size(); - for (int s = 0; s < sessions; s++) { - XYSeries series = new XYSeries(s == sessions - 1 ? "Latest" : "S" + s); - List sessionRow = allSkillValues.get(s); - for (int k = 0; k < idxs.size(); k++) { - int colIndex = idxs.get(k); - int x = k + 1; - Integer vv = (colIndex < sessionRow.size() ? sessionRow.get(colIndex) : null); - double y = (double) (vv == null ? 0 : vv); - series.add(x, addJitter(y)); - } - dataset.addSeries(series); - } - - JFreeChart subchart = ChartFactory.createXYLineChart( - grp + " - " + (idxs.size()) + " items", - "Skill", - "Value", - dataset, - PlotOrientation.VERTICAL, - false, - true, - false - ); - XYPlot plot = subchart.getXYPlot(); - plot.setBackgroundPaint(Color.WHITE); - XYLineAndShapeRenderer renderer = new XYLineAndShapeRenderer(); - for (int s = 0; s < dataset.getSeriesCount(); s++) { - if (s == dataset.getSeriesCount() - 1) { - renderer.setSeriesPaint(s, Color.BLACK); - renderer.setSeriesStroke(s, new BasicStroke(2.5f)); - renderer.setSeriesShapesVisible(s, true); - renderer.setSeriesShape(s, new java.awt.geom.Ellipse2D.Double(-4, -4, 8, 8)); - } else { - renderer.setSeriesPaint(s, Color.GRAY); - renderer.setSeriesStroke(s, new BasicStroke(1.5f)); - renderer.setSeriesShapesVisible(s, false); - } - } - plot.setRenderer(renderer); - // Ensure Y axis range and ticks show 0..3 grid with a small lower padding for x-axis visibility - try { - NumberAxis yAxis = (NumberAxis) plot.getRangeAxis(); - yAxis.setRange(-0.25, 4.25); - yAxis.setTickUnit(new org.jfree.chart.axis.NumberTickUnit(1)); - } catch (ClassCastException cce) { - LOG.debug("Range axis is not a NumberAxis: {}", cce.toString()); - } - NumberAxis domain = (NumberAxis) plot.getDomainAxis(); - if (idxs.size() <= 1) { - // single-point chart: give a small visual range around the point - domain.setRange(0.5, 1.5); - } else { - domain.setRange(1, idxs.size()); - } - - ChartPanel cp = new ChartPanel(subchart); - // Store the group id on the panel so callers can name files per-group - cp.setName(grp); - cp.setPreferredSize(new Dimension(800, Math.max(100, 40 * idxs.size()))); - cp.setMaximumSize(new Dimension(Integer.MAX_VALUE, cp.getPreferredSize().height)); - multiChartContainer.add(cp); - } - - add(new javax.swing.JScrollPane(multiChartContainer), BorderLayout.CENTER); - revalidate(); - repaint(); - } - - /** - * Plot grouped data over time. Dates are used as the X axis (oldest first). - * Each skill within a group is drawn as its own line (one series per skill) - * with point markers and a color-blind friendly palette. Legend placed - * in the upper-right corner. - * - * @param dates chronological list of session dates (oldest first) - * @param rows list of session rows where each row is a list of integer scores - * @param partCodes array of part codes aligned with the columns in each row - */ - public void updateWithGroupedDataByDate(final java.util.List dates, final java.util.List> rows, final String[] partCodes) { - // Backwards-compatible wrapper: use code strings as labels if caller didn't provide labels - String[] labels = partCodes == null ? null : partCodes.clone(); - updateWithGroupedDataByDate(dates, rows, partCodes, labels); - } - - /** - * Plot grouped data over time with optional human-friendly labels. - * Each provided {@code partCodes} entry maps to a column index inside - * {@code rows} and (optionally) a friendly label supplied in - * {@code partLabels}. The dates list must be ordered oldest-first and - * must be parallel to the rows list. - * - * @param dates chronological list of session dates (oldest first) - * @param rows list of session rows where each row is a list of integer scores - * @param partCodes array of part codes aligned with the columns in each row - * @param partLabels optional human friendly labels parallel to {@code partCodes} - */ - public void updateWithGroupedDataByDate(final java.util.List dates, final java.util.List> rows, final String[] partCodes, final String[] partLabels) { - LOG.debug("updateWithGroupedDataByDate called with dates={} rows={} parts={}", dates == null ? 0 : dates.size(), rows == null ? 0 : rows.size(), partCodes == null ? 0 : partCodes.length); - if (dates == null || rows == null || partCodes == null) { - return; - } - // Build groups preserving order - java.util.LinkedHashMap> groups = new java.util.LinkedHashMap<>(); - for (int i = 0; i < partCodes.length; i++) { - String code = partCodes[i]; - String grp = code != null && code.contains("_") ? code.split("_")[0] : code; - groups.computeIfAbsent(grp, k -> new java.util.ArrayList<>()).add(i); - } - - // Remove any single chart mode UI - removeAll(); - multiChartContainer = new javax.swing.JPanel(); - multiChartContainer.setLayout(new javax.swing.BoxLayout(multiChartContainer, javax.swing.BoxLayout.Y_AXIS)); - - // Color-blind friendly palette (ColorBrewer Set2-like) - java.awt.Color[] palette = new java.awt.Color[] { - new java.awt.Color(0x1b9e77), // green - new java.awt.Color(0xd95f02), // orange - new java.awt.Color(0x7570b3), // purple - new java.awt.Color(0xe7298a), // pink - new java.awt.Color(0x66a61e), // olive - new java.awt.Color(0xe6ab02), // mustard - new java.awt.Color(0xa6761d), // brown - new java.awt.Color(0x666666) // gray - }; - - for (var entry : groups.entrySet()) { - String grp = entry.getKey(); - java.util.List idxs = entry.getValue(); - org.jfree.data.time.TimeSeriesCollection dataset = new org.jfree.data.time.TimeSeriesCollection(); - - // For each skill in the group, build a time series across dates - for (int k = 0; k < idxs.size(); k++) { - int colIndex = idxs.get(k); - String code = partCodes[colIndex]; - String human = (partLabels != null && partLabels.length > colIndex && partLabels[colIndex] != null) ? partLabels[colIndex] : code; - String seriesName = code + " - " + human; // legend shows code plus friendly label - org.jfree.data.time.TimeSeries ts = new org.jfree.data.time.TimeSeries(seriesName); - for (int r = 0; r < rows.size(); r++) { - java.time.LocalDate d = dates.get(r); - java.util.List row = rows.get(r); - Integer vv = (colIndex < row.size()) ? row.get(colIndex) : null; - double val = (double) (vv == null ? 0 : vv); - org.jfree.data.time.Day day = new org.jfree.data.time.Day(java.util.Date.from(d.atStartOfDay(java.time.ZoneId.systemDefault()).toInstant())); - ts.addOrUpdate(day, addJitter(val)); - } - dataset.addSeries(ts); - } - - // Title: "Phase N Progression" when grp matches P - String title = (grp != null && grp.startsWith("P") && grp.length() > 1) - ? ("Phase " + grp.substring(1) + " Progression") - : (grp + " progression"); - - JFreeChart subchart = ChartFactory.createTimeSeriesChart( - title, - "Date", - "Value", - dataset, - true, - true, - false - ); - - XYPlot plot = subchart.getXYPlot(); - plot.setBackgroundPaint(java.awt.Color.WHITE); - // Add colored horizontal bands behind the data using polygon annotations - try { - // Compute domain lower/upper bounds in millis for the current dataset if available - long domainLower = Long.MIN_VALUE; - long domainUpper = Long.MAX_VALUE; - if (!dates.isEmpty()) { - java.time.ZoneId zid = java.time.ZoneId.systemDefault(); - java.time.LocalDate first = dates.get(0); - java.time.LocalDate last = dates.get(dates.size() - 1).plusDays(4); - domainLower = java.util.Date.from(first.atStartOfDay(zid).toInstant()).getTime(); - domainUpper = java.util.Date.from(last.atStartOfDay(zid).toInstant()).getTime(); - } - double left = domainLower == Long.MIN_VALUE ? plot.getDomainAxis().getRange().getLowerBound() : domainLower; - double right = domainUpper == Long.MAX_VALUE ? plot.getDomainAxis().getRange().getUpperBound() : domainUpper; - // Use shared helper to draw bands in the domain coordinates (millis) - addHorizontalBands(plot, left, right); - } catch (Throwable t) { - LOG.debug("Unable to add background bands as annotations: {}", t.toString()); - } - org.jfree.chart.renderer.xy.XYLineAndShapeRenderer renderer = new org.jfree.chart.renderer.xy.XYLineAndShapeRenderer(true, true); - // assign colors and markers - for (int s = 0; s < dataset.getSeriesCount(); s++) { - java.awt.Color c = palette[s % palette.length]; - renderer.setSeriesPaint(s, c); - renderer.setSeriesStroke(s, new java.awt.BasicStroke(2.0f)); - renderer.setSeriesShapesVisible(s, true); - renderer.setSeriesShape(s, new java.awt.geom.Ellipse2D.Double(-3, -3, 6, 6)); - } - plot.setRenderer(renderer); - - // Ensure Y axis range and ticks show 0..3 grid with a small lower padding for x-axis visibility - try { - NumberAxis yAxis = (NumberAxis) plot.getRangeAxis(); - yAxis.setRange(-0.25, 4.25); - yAxis.setTickUnit(new org.jfree.chart.axis.NumberTickUnit(1)); - } catch (ClassCastException cce) { - LOG.debug("Range axis is not a NumberAxis: {}", cce.toString()); - } - - // Ensure Y axis range and ticks show 0..3 grid with a small lower padding for x-axis visibility - try { - org.jfree.chart.axis.DateAxis dateAxis = (org.jfree.chart.axis.DateAxis) plot.getDomainAxis(); - java.text.SimpleDateFormat fmt = new java.text.SimpleDateFormat("yyyyMMdd"); - dateAxis.setDateFormatOverride(fmt); - // Use the provided dates list to determine bounds (oldest first) - if (!dates.isEmpty()) { - java.time.ZoneId zid = java.time.ZoneId.systemDefault(); - java.time.LocalDate firstDate = dates.get(0); - java.time.LocalDate lastDate = dates.get(dates.size() - 1); - // pad 4 days on the right to provide visual breathing room - java.time.LocalDate paddedUpper = lastDate.plusDays(4); - java.util.Date lower = java.util.Date.from(firstDate.atStartOfDay(zid).toInstant()); - java.util.Date upper = java.util.Date.from(paddedUpper.atStartOfDay(zid).toInstant()); - dateAxis.setRange(lower, upper); - // one-day tick units so each datapoint maps to a single label - dateAxis.setTickUnit(new org.jfree.chart.axis.DateTickUnit(org.jfree.chart.axis.DateTickUnitType.DAY, 1)); - } - } catch (ClassCastException cce) { - LOG.debug("Domain axis is not a DateAxis: {}", cce.toString()); - } - - // Place legend below the plot for clarity and allow it to show codes+labels - if (subchart.getLegend() != null) { - subchart.getLegend().setPosition(org.jfree.chart.ui.RectangleEdge.BOTTOM); - } - - ChartPanel cp = new ChartPanel(subchart); - cp.setName(grp); - cp.setPreferredSize(new Dimension(1000, Math.max(180, 40 * idxs.size()))); - cp.setMaximumSize(new Dimension(Integer.MAX_VALUE, cp.getPreferredSize().height)); - multiChartContainer.add(cp); - } - - add(new javax.swing.JScrollPane(multiChartContainer), BorderLayout.CENTER); - revalidate(); - repaint(); - } - - /** - * Save each grouped subchart as an individual PNG file. The method writes - * files named {baseName}-{group}.png into the provided directory and - * returns a map of group -> written path. Caller must ensure grouped data - * has been rendered (updateWithGroupedData called) prior to invoking this. - * - * @param dir directory to write files into - * @param baseName base filename (no extension) to prefix each file - * @param width image width in pixels - * @param heightPerGroup per-group image height in pixels - * @return ordered map of group id to written file path - * @throws java.io.IOException on I/O error - */ - public java.util.Map saveGroupedCharts(final java.nio.file.Path dir, final String baseName, final int width, final int heightPerGroup) throws java.io.IOException { - java.util.Map out = new java.util.LinkedHashMap<>(); - if (dir == null) { - throw new java.io.IOException("output dir is null"); - } - java.nio.file.Files.createDirectories(dir); - if (multiChartContainer == null || multiChartContainer.getComponentCount() == 0) { - return out; - } - for (int i = 0; i < multiChartContainer.getComponentCount(); i++) { - java.awt.Component c = multiChartContainer.getComponent(i); - String grp = c.getName() != null ? c.getName() : String.valueOf(i+1); - int h = Math.max(100, heightPerGroup); - c.setSize(width, h); - c.doLayout(); - java.awt.image.BufferedImage img = new java.awt.image.BufferedImage(width, h, java.awt.image.BufferedImage.TYPE_INT_ARGB); - java.awt.Graphics2D g = img.createGraphics(); - g.setColor(java.awt.Color.WHITE); - g.fillRect(0, 0, width, h); - c.paint(g); - g.dispose(); - java.nio.file.Path file = dir.resolve(baseName + "-" + grp + ".png"); - try (java.io.OutputStream os = java.nio.file.Files.newOutputStream(file); - javax.imageio.stream.ImageOutputStream ios = javax.imageio.ImageIO.createImageOutputStream(os)) { - boolean written = javax.imageio.ImageIO.write(img, "png", ios); - if (!written) { - throw new java.io.IOException("No ImageWriter for png"); - } - } - out.put(grp, file); - } - return out; - } - - /** - * Show an empty grouped chart using the provided part codes. This will - * render one row of zeros sized to the number of parts so the UI shows - * grouped axes and placeholders even when no session data exists yet. - * - * @param partCodes array of part codes used to determine the number of columns - */ - public void showEmptyGrouped(final String[] partCodes) { - if (partCodes == null) { - return; - } - List zeros = new java.util.ArrayList<>(java.util.Collections.nCopies(partCodes.length, 0)); - List> rows = new java.util.ArrayList<>(); - rows.add(zeros); - updateWithGroupedData(rows, partCodes); - } - - /** - * Save the current chart to a PNG file. If the chart is empty this will - * still export the rendered chart panel contents. - * - * @param outputPath path to write the PNG file to - * @param width image width in pixels - * @param height image height in pixels - * @throws java.io.IOException if writing fails - */ - public void saveChart(final java.nio.file.Path outputPath, final int width, final int height) throws java.io.IOException { - if (outputPath == null) { - throw new java.io.IOException("outputPath is null"); - } - java.nio.file.Path parent = outputPath.getParent(); - if (parent == null) { - parent = java.nio.file.Paths.get("."); - } - // Ensure parent directory exists - java.nio.file.Files.createDirectories(parent); - java.awt.image.BufferedImage img = null; - // If we are in grouped-chart mode, render the multiChartContainer component - if (multiChartContainer != null && multiChartContainer.getComponentCount() > 0) { - // Ensure layout sizes are applied - multiChartContainer.setSize(width, height); - multiChartContainer.doLayout(); - img = new java.awt.image.BufferedImage(width, height, java.awt.image.BufferedImage.TYPE_INT_ARGB); - java.awt.Graphics2D g = img.createGraphics(); - // paint background white to match chart look - g.setColor(java.awt.Color.WHITE); - g.fillRect(0, 0, width, height); - multiChartContainer.paint(g); - g.dispose(); - } else if (chart != null) { - img = chart.createBufferedImage(width, height); - } else { - throw new java.io.IOException("No chart available to render"); - } - - try { - // Use an explicit OutputStream -> ImageOutputStream to avoid platform-specific ImageIO issues - try (java.io.OutputStream os = java.nio.file.Files.newOutputStream(outputPath); - javax.imageio.stream.ImageOutputStream ios = javax.imageio.ImageIO.createImageOutputStream(os)) { - boolean written = javax.imageio.ImageIO.write(img, "png", ios); - if (!written) { - throw new java.io.IOException("No ImageWriter available for format 'png'"); - } - } - } catch (java.io.IOException ioe) { - String diag = String.format("Failed saving chart to %s (parentExists=%b, parentWritable=%b, parentIsDir=%b)", - outputPath.toString(), java.nio.file.Files.exists(parent), java.nio.file.Files.isWritable(parent), java.nio.file.Files.isDirectory(parent)); - throw new java.io.IOException(diag, ioe); - } - } - - private void updateXAxisLabels() { - // Generate labels for the X-axis - String[] skillLabels = new String[NUMBER_OF_SKILLS]; - int skillGroup = 1; - int skillNumber = 1; - for (int i = 0; i < NUMBER_OF_SKILLS; i++) { - skillLabels[i] = "Skill" + skillGroup + "-" + skillNumber; - skillNumber++; - if ((skillGroup == 1 && skillNumber > 6) || - (skillGroup == 2 && skillNumber > 4) || - (skillGroup == 3 && skillNumber > 11) || - (skillGroup == 4 && skillNumber > 7)) { - skillGroup++; - skillNumber = 1; - } - } - - // Set the custom labels on the X-axis - NumberAxis domain = (NumberAxis) chart.getXYPlot().getDomainAxis(); - domain.setVerticalTickLabels(true); - domain.setTickLabelFont(new Font("SansSerif", Font.PLAIN, 8)); - domain.setTickUnit(new org.jfree.chart.axis.NumberTickUnit(1) { - @Override - public String valueToString(double value) { - int index = (int) value - 1; - if (index >= 0 && index < skillLabels.length) { - return skillLabels[index]; - } - return ""; - } - }); - } -} +package com.studentgui.apppages; + +import java.awt.BasicStroke; +import java.awt.BorderLayout; +import java.awt.Color; +import java.awt.Dimension; +import java.awt.Font; +import java.util.List; +import java.util.Random; +import java.util.concurrent.ThreadLocalRandom; + +import javax.swing.JPanel; + +import org.jfree.chart.ChartFactory; +import org.jfree.chart.ChartPanel; +import org.jfree.chart.JFreeChart; +import org.jfree.chart.annotations.XYPolygonAnnotation; +import org.jfree.chart.axis.NumberAxis; +import org.jfree.chart.plot.PlotOrientation; +import org.jfree.chart.plot.XYPlot; +import org.jfree.chart.renderer.xy.XYLineAndShapeRenderer; +import org.jfree.data.xy.XYSeries; +import org.jfree.data.xy.XYSeriesCollection; + +/** + * Reusable JFreeChart-based line chart component for visualizing student assessment progress. + * + *

This component is shared across all assessment pages (Braille, Abacus, iOS, ScreenReader, etc.) + * to display time-series data showing skill progression over multiple sessions. It supports three + * primary visualization modes:

+ * + *
    + *
  • Single-chart mode: {@link #updateWithData(java.util.List)} - Plots all skills on one + * chart with historical sessions in gray and the latest session highlighted in black
  • + *
  • Grouped mode (session indices): {@link #updateWithGroupedData(java.util.List, String[])} - + * Creates multiple stacked charts, one per phase group (determined by part code prefix like "P1", "P2")
  • + *
  • Grouped mode (chronological dates): {@link #updateWithGroupedDataByDate(java.util.List, java.util.List, String[], String[])} - + * Plots grouped data with actual dates on the X-axis for true time-series visualization
  • + *
+ * + *

Visual Design and Rendering:

+ *
    + *
  • Background bands: Colored horizontal bands indicate score ranges to aid interpretation: + *
      + *
    • Red band: -0.25 to 0.5 (minimal/no proficiency)
    • + *
    • Orange bands: 0.5\u20131.5, 1.5\u20132.5 (emerging skills)
    • + *
    • Yellow band: 2.5\u20133.5 (developing proficiency)
    • + *
    • Green band: 3.5\u20134.5 (mastery/proficient)
    • + *
    + *
  • + *
  • Rendering jitter: A configurable visual jitter of ±{@value #JITTER_AMPLITUDE} is applied + * to plotted points via {@link #addJitter(double)} to reveal overlapping data points. This is a + * display-only transformation and does not modify persisted values. Jitter can be: + *
      + *
    • Enabled/disabled via {@link #setJitterEnabled(boolean)}
    • + *
    • Made deterministic (for testing) via {@link #setJitterDeterministic(boolean)} and {@link #setJitterSeed(Long)}
    • + *
    • Configured via {@link com.studentgui.apphelpers.Settings} keys: "jitter.enabled", "jitter.deterministic", "jitter.seed"
    • + *
    + *
  • + *
  • Color palette: Consistent color-blind friendly palette used for series rendering: + *
      + *
    • {@link #PALETTE_HEX}: Hex color strings for HTML legend generation (8 colors)
    • + *
    • {@link #PALETTE}: AWT Color objects for JFreeChart rendering (8 colors matching PALETTE_HEX)
    • + *
    + *
  • + *
+ * + *

Typical Workflow for Assessment Pages:

+ *
    + *
  1. Page fetches recent sessions from database via {@link com.studentgui.apphelpers.Database#fetchLatestAssessmentResultsWithDates}
  2. + *
  3. Page calls {@link #updateWithGroupedDataByDate(java.util.List, java.util.List, String[], String[])} to populate chart
  4. + *
  5. On submit, page calls {@link #saveGroupedCharts(java.nio.file.Path, String, int, int)} to export PNG images
  6. + *
  7. Page generates Markdown/HTML reports linking to the exported plots
  8. + *
+ * + *

Export and Persistence:

+ *
    + *
  • {@link #saveGroupedCharts(java.nio.file.Path, String, int, int)} - Exports each phase group as a separate PNG file
  • + *
  • {@link #saveChart(java.nio.file.Path, int, int)} - Exports the single main chart (when not in grouped mode)
  • + *
  • Returns Map<groupName, filePath> for use in report generation
  • + *
+ * + *

Accessibility:

+ *
    + *
  • ChartPanel accessible name set to "Skill progression chart"
  • + *
  • Tooltips enabled showing coordinate values on hover
  • + *
  • Keyboard navigation supported through JFreeChart's default ChartPanel behavior
  • + *
+ * + *

Settings Integration: Implements {@link com.studentgui.app.SettingsChangeListener} to respond + * to jitter configuration changes at runtime without requiring application restart.

+ * + * @see com.studentgui.apphelpers.Database#fetchLatestAssessmentResultsWithDates + * @see com.studentgui.app.SettingsChangeListener + * @see org.jfree.chart.JFreeChart + * @see org.jfree.chart.ChartPanel + */ +public class JLineGraph extends JPanel implements com.studentgui.app.SettingsChangeListener { + private static final long serialVersionUID = 1L; + private static final org.slf4j.Logger LOG = org.slf4j.LoggerFactory.getLogger(JLineGraph.class); + /** The dataset containing XY series for historical and latest sessions. */ + private final XYSeriesCollection lineDataset; + /** The JFreeChart instance used to render the plot. */ + private final JFreeChart chart; + /** Panel that embeds the chart and provides UI features. */ + private final ChartPanel chartPanel; + /** When rendering grouped charts we place multiple ChartPanels in this container. */ + private javax.swing.JPanel multiChartContainer; + /** Domain axis used to customise X-axis labels and range. */ + private final NumberAxis xAxis; + /** Expected number of skill columns per session. */ + private static final int NUMBER_OF_SKILLS = 28; // Adjust as needed + /** Jitter amplitude (plus/minus) applied to plotted data points. */ + private static final double JITTER_AMPLITUDE = 0.10d; + + /** Whether rendering jitter is currently enabled. Default: true. */ + private boolean jitterEnabled = true; + /** When true, use a deterministic java.util.Random seeded RNG instead of ThreadLocalRandom. */ + private boolean jitterDeterministic = false; + /** Optional seed used when deterministic jitter is enabled. */ + private Long jitterSeed = null; + /** Cached Random instance when deterministic mode is enabled. */ + private Random deterministicRandom = null; + + /** + * Add a small random jitter within +/- JITTER_AMPLITUDE to the provided value. + * When jitter is disabled this returns the original value unchanged. + */ + private double addJitter(final double v) { + if (!jitterEnabled) { + return v; + } + try { + if (jitterDeterministic) { + if (deterministicRandom == null) { + long seed = jitterSeed == null ? 0L : jitterSeed.longValue(); + deterministicRandom = new Random(seed); + } + double r = deterministicRandom.nextDouble() * 2.0 - 1.0; // -1..1 + return v + (r * JITTER_AMPLITUDE); + } else { + return v + ThreadLocalRandom.current().nextDouble(-JITTER_AMPLITUDE, JITTER_AMPLITUDE); + } + } catch (Throwable t) { + // In the unlikely event RNG is unavailable, fall back to no jitter + return v; + } + } + /** Public color palette (hex) for HTML legends and consistency across pages. */ + public static final String[] PALETTE_HEX = new String[] { + "#1b9e77","#d95f02","#7570b3","#e7298a","#66a61e","#e6ab02","#a6761d","#666666" + }; + /** Public color palette as AWT Color objects for chart rendering. */ + public static final java.awt.Color[] PALETTE = new java.awt.Color[] { + new java.awt.Color(0x1b9e77), + new java.awt.Color(0xd95f02), + new java.awt.Color(0x7570b3), + new java.awt.Color(0xe7298a), + new java.awt.Color(0x66a61e), + new java.awt.Color(0xe6ab02), + new java.awt.Color(0xa6761d), + new java.awt.Color(0x666666) + }; + + /** + * Create a new JLineGraph with default styling and an empty dataset. + */ + public JLineGraph() { + setLayout(new BorderLayout()); + lineDataset = new XYSeriesCollection(); + + // Create a chart + chart = ChartFactory.createXYLineChart( + "Skill Progression", + "Skills", + "Value", + lineDataset, + PlotOrientation.VERTICAL, + true, + true, + false + ); + + // Customize the plot + XYPlot plot = chart.getXYPlot(); + plot.setBackgroundPaint(Color.WHITE); + plot.setDomainGridlinePaint(Color.GRAY); + plot.setRangeGridlinePaint(Color.GRAY); + + // Set axis ranges + xAxis = (NumberAxis) plot.getDomainAxis(); + xAxis.setRange(0, NUMBER_OF_SKILLS + 1); + NumberAxis yAxis = (NumberAxis) plot.getRangeAxis(); + yAxis.setRange(-0.25, 4.25); + + // Create background bands + addBackgroundBands(plot); + + chartPanel = new ChartPanel(chart); + chartPanel.setPreferredSize(new Dimension(800, 600)); + chartPanel.getAccessibleContext().setAccessibleName("Skill progression chart"); + chartPanel.setToolTipText("Skill progression chart showing historical and latest values"); + add(chartPanel, BorderLayout.CENTER); + multiChartContainer = null; + + // Set custom X-axis labels + updateXAxisLabels(); + // Apply any persisted settings at creation time + try { + settingsChanged(); + } catch (Throwable t) { + // ignore any issues reading settings at startup + } + } + + @Override + public void settingsChanged() { + try { + String je = com.studentgui.apphelpers.Settings.get("jitter.enabled", String.valueOf(this.jitterEnabled)); + setJitterEnabled("true".equalsIgnoreCase(je)); + String jd = com.studentgui.apphelpers.Settings.get("jitter.deterministic", String.valueOf(this.jitterDeterministic)); + setJitterDeterministic("true".equalsIgnoreCase(jd)); + String s = com.studentgui.apphelpers.Settings.get("jitter.seed", this.jitterSeed == null ? "" : String.valueOf(this.jitterSeed)); + if (s == null || s.trim().isEmpty()) { + setJitterSeed(null); + } else { + try { + long v = Long.parseLong(s.trim()); + setJitterSeed(Long.valueOf(v)); + } catch (NumberFormatException nfe) { + setJitterSeed(null); + } + } + // reset cached RNG so seed/cfg takes effect + this.deterministicRandom = null; + if (chart != null) { + chart.fireChartChanged(); + } + if (chartPanel != null) { + chartPanel.repaint(); + } + } catch (Throwable t) { + LOG.debug("Failed applying settings: {}", t.toString()); + } + } + + /** + * Add lightly-colored horizontal bands to the plot to indicate score + * ranges. + */ + private void addBackgroundBands(final XYPlot plot) { + // Use the generic band painter to draw the requested bands across the + // full X domain of the main chart. + double left = 0.0; + double right = NUMBER_OF_SKILLS + 1; + addHorizontalBands(plot, left, right); + } + + /** + * Add horizontal background bands to the provided plot between left and right + * X coordinates. Bands follow the requested ranges: + * red = -0.25..0.5, orange = 0.5..1.5, orange = 1.5..2.5, yellow = 2.5..3.5, + * green = 3.5..4.5 + */ + private void addHorizontalBands(final XYPlot plot, final double left, final double right) { + try { + java.awt.Color red = new java.awt.Color(255, 0, 0, 40); + java.awt.Color orange = new java.awt.Color(255, 165, 0, 40); + java.awt.Color orange2 = new java.awt.Color(255, 140, 0, 40); + java.awt.Color yellow = new java.awt.Color(255, 255, 0, 40); + java.awt.Color green = new java.awt.Color(0, 255, 0, 40); + + double[][] bands = new double[][]{ + { -0.25, 0.5 }, + { 0.5, 1.5 }, + { 1.5, 2.5 }, + { 2.5, 3.5 }, + { 3.5, 4.5 } + }; + java.awt.Color[] colors = new java.awt.Color[] { red, orange, orange2, yellow, green }; + + for (int i = 0; i < bands.length; i++) { + double low = bands[i][0]; + double high = bands[i][1]; + double[] coords = new double[] { left, low, right, low, right, high, left, high }; + plot.addAnnotation(new XYPolygonAnnotation(coords, null, null, colors[i])); + } + } catch (Throwable t) { + LOG.debug("Unable to add horizontal bands: {}", t.toString()); + } + } + + /** + * Enable or disable rendering jitter at runtime. + * @param enabled true to enable jitter, false to draw raw values + */ + public void setJitterEnabled(final boolean enabled) { + this.jitterEnabled = enabled; + } + + /** + * Query whether rendering jitter is currently enabled. + * + * @return true when jitter is enabled, false otherwise + */ + public boolean isJitterEnabled() { + return this.jitterEnabled; + } + + /** + * Enable/disable deterministic (seeded) jitter. + * When enabled, jitter will be generated from a java.util.Random seeded + * with {@link #jitterSeed} (or 0 when seed is null). + * + * @param deterministic true to use a seeded RNG, false to use non-deterministic RNG + */ + public void setJitterDeterministic(final boolean deterministic) { + this.jitterDeterministic = deterministic; + this.deterministicRandom = null; // reset instance so seed takes effect + } + + /** + * Query whether deterministic jitter is enabled. + * + * @return true when deterministic (seeded) jitter is enabled + */ + public boolean isJitterDeterministic() { + return this.jitterDeterministic; + } + + /** + * Set the seed used when deterministic jitter is enabled. Pass null to + * clear the seed (will use 0 when a deterministic RNG is created). + * + * @param seed seed value or null to clear + */ + public void setJitterSeed(final Long seed) { + this.jitterSeed = seed; + this.deterministicRandom = null; + } + + /** + * Return the currently configured jitter seed or null when unset. + * + * @return configured seed value or null when not set + */ + public Long getJitterSeed() { + return this.jitterSeed; + } + + /** + * Replace the current dataset with the provided list of skill value + * series. Each inner list represents a single session and must contain + * NUMBER_OF_SKILLS entries. + * + * @param allSkillValues list of sessions where each session is a list of + * integer skill values (older sessions first) + */ + public void updateWithData(final List> allSkillValues) { + LOG.debug("updateWithData called with {} rows", allSkillValues == null ? 0 : allSkillValues.size()); + if (allSkillValues == null || allSkillValues.isEmpty()) { + return; + } + // Fallback to existing single-chart behavior + lineDataset.removeAllSeries(); + XYLineAndShapeRenderer renderer = new XYLineAndShapeRenderer(); + + // Add historical data series (each prior session as a separate series) + for (int s = 0; s < allSkillValues.size() - 1; s++) { + XYSeries hs = new XYSeries("S" + s); + List skillValues = allSkillValues.get(s); + if (skillValues == null) { + continue; + } + for (int j = 0; j < skillValues.size(); j++) { + Integer v = skillValues.get(j); + double y = (double) (v == null ? 0 : v); + hs.add(j + 1, addJitter(y)); + } + lineDataset.addSeries(hs); + renderer.setSeriesPaint(s, Color.GRAY); + renderer.setSeriesStroke(s, new BasicStroke(2.0f)); + renderer.setSeriesShapesVisible(s, false); + } + + // Latest session + XYSeries latestSeries = new XYSeries("Latest"); + List latestSkillValues = allSkillValues.get(allSkillValues.size() - 1); + if (latestSkillValues != null) { + for (int i = 0; i < latestSkillValues.size(); i++) { + Integer v = latestSkillValues.get(i); + double y = (double) (v == null ? 0 : v); + latestSeries.add(i + 1, addJitter(y)); + } + } + lineDataset.addSeries(latestSeries); + int latestIndex = lineDataset.getSeriesCount() - 1; + renderer.setSeriesPaint(latestIndex, Color.BLACK); + renderer.setSeriesStroke(latestIndex, new BasicStroke(3f)); + renderer.setSeriesShapesVisible(latestIndex, true); + renderer.setSeriesShape(latestIndex, new java.awt.geom.Ellipse2D.Double(-6, -6, 12, 12)); + + chart.getXYPlot().setDataset(lineDataset); + chart.getXYPlot().setRenderer(renderer); + // Ensure Y axis range and ticks are consistent across charts + try { + NumberAxis y = (NumberAxis) chart.getXYPlot().getRangeAxis(); + y.setRange(-0.25, 4.25); + y.setTickUnit(new org.jfree.chart.axis.NumberTickUnit(1)); + } catch (ClassCastException ignored) { + // if range axis isn't a NumberAxis, ignore + } + chart.fireChartChanged(); + chartPanel.repaint(); + } + + /** + * Update the component with grouped plots. Each group is determined by the + * prefix of the part code (e.g. 'P1' from 'P1_1'). For each group we render + * a separate small chart stacked vertically. + * + * @param allSkillValues list of sessions (older first) where each session is a list of integer skill values + * @param partCodes array of part codes aligned with columns in each session row + */ + public void updateWithGroupedData(final List> allSkillValues, final String[] partCodes) { + LOG.debug("updateWithGroupedData called with rows={} partCodes={}", allSkillValues == null ? 0 : allSkillValues.size(), partCodes == null ? 0 : partCodes.length); + // validate + if (partCodes == null || partCodes.length == 0 || allSkillValues == null || allSkillValues.isEmpty()) { + return; + } + + // Build group -> indexes map preserving order of first occurrence + java.util.LinkedHashMap> groups = new java.util.LinkedHashMap<>(); + for (int i = 0; i < partCodes.length; i++) { + String code = partCodes[i]; + String grp = code != null && code.contains("_") ? code.split("_")[0] : code; + groups.computeIfAbsent(grp, k -> new java.util.ArrayList<>()).add(i); + } + + // Remove any single chart mode UI + removeAll(); + multiChartContainer = new javax.swing.JPanel(); + multiChartContainer.setLayout(new javax.swing.BoxLayout(multiChartContainer, javax.swing.BoxLayout.Y_AXIS)); + + // For each group create a small chart + for (var entry : groups.entrySet()) { + String grp = entry.getKey(); + java.util.List idxs = entry.getValue(); + XYSeriesCollection dataset = new XYSeriesCollection(); + // historical sessions: create one series per prior session + int sessions = allSkillValues.size(); + for (int s = 0; s < sessions; s++) { + XYSeries series = new XYSeries(s == sessions - 1 ? "Latest" : "S" + s); + List sessionRow = allSkillValues.get(s); + for (int k = 0; k < idxs.size(); k++) { + int colIndex = idxs.get(k); + int x = k + 1; + Integer vv = (colIndex < sessionRow.size() ? sessionRow.get(colIndex) : null); + double y = (double) (vv == null ? 0 : vv); + series.add(x, addJitter(y)); + } + dataset.addSeries(series); + } + + JFreeChart subchart = ChartFactory.createXYLineChart( + grp + " - " + (idxs.size()) + " items", + "Skill", + "Value", + dataset, + PlotOrientation.VERTICAL, + false, + true, + false + ); + XYPlot plot = subchart.getXYPlot(); + plot.setBackgroundPaint(Color.WHITE); + XYLineAndShapeRenderer renderer = new XYLineAndShapeRenderer(); + for (int s = 0; s < dataset.getSeriesCount(); s++) { + if (s == dataset.getSeriesCount() - 1) { + renderer.setSeriesPaint(s, Color.BLACK); + renderer.setSeriesStroke(s, new BasicStroke(2.5f)); + renderer.setSeriesShapesVisible(s, true); + renderer.setSeriesShape(s, new java.awt.geom.Ellipse2D.Double(-4, -4, 8, 8)); + } else { + renderer.setSeriesPaint(s, Color.GRAY); + renderer.setSeriesStroke(s, new BasicStroke(1.5f)); + renderer.setSeriesShapesVisible(s, false); + } + } + plot.setRenderer(renderer); + // Ensure Y axis range and ticks show 0..3 grid with a small lower padding for x-axis visibility + try { + NumberAxis yAxis = (NumberAxis) plot.getRangeAxis(); + yAxis.setRange(-0.25, 4.25); + yAxis.setTickUnit(new org.jfree.chart.axis.NumberTickUnit(1)); + } catch (ClassCastException cce) { + LOG.debug("Range axis is not a NumberAxis: {}", cce.toString()); + } + NumberAxis domain = (NumberAxis) plot.getDomainAxis(); + if (idxs.size() <= 1) { + // single-point chart: give a small visual range around the point + domain.setRange(0.5, 1.5); + } else { + domain.setRange(1, idxs.size()); + } + + ChartPanel cp = new ChartPanel(subchart); + // Store the group id on the panel so callers can name files per-group + cp.setName(grp); + cp.setPreferredSize(new Dimension(800, Math.max(100, 40 * idxs.size()))); + cp.setMaximumSize(new Dimension(Integer.MAX_VALUE, cp.getPreferredSize().height)); + multiChartContainer.add(cp); + } + + add(new javax.swing.JScrollPane(multiChartContainer), BorderLayout.CENTER); + revalidate(); + repaint(); + } + + /** + * Plot grouped data over time. Dates are used as the X axis (oldest first). + * Each skill within a group is drawn as its own line (one series per skill) + * with point markers and a color-blind friendly palette. Legend placed + * in the upper-right corner. + * + * @param dates chronological list of session dates (oldest first) + * @param rows list of session rows where each row is a list of integer scores + * @param partCodes array of part codes aligned with the columns in each row + */ + public void updateWithGroupedDataByDate(final java.util.List dates, final java.util.List> rows, final String[] partCodes) { + // Backwards-compatible wrapper: use code strings as labels if caller didn't provide labels + String[] labels = partCodes == null ? null : partCodes.clone(); + updateWithGroupedDataByDate(dates, rows, partCodes, labels); + } + + /** + * Plot grouped data over time with optional human-friendly labels. + * Each provided {@code partCodes} entry maps to a column index inside + * {@code rows} and (optionally) a friendly label supplied in + * {@code partLabels}. The dates list must be ordered oldest-first and + * must be parallel to the rows list. + * + * @param dates chronological list of session dates (oldest first) + * @param rows list of session rows where each row is a list of integer scores + * @param partCodes array of part codes aligned with the columns in each row + * @param partLabels optional human friendly labels parallel to {@code partCodes} + */ + public void updateWithGroupedDataByDate(final java.util.List dates, final java.util.List> rows, final String[] partCodes, final String[] partLabels) { + LOG.debug("updateWithGroupedDataByDate called with dates={} rows={} parts={}", dates == null ? 0 : dates.size(), rows == null ? 0 : rows.size(), partCodes == null ? 0 : partCodes.length); + if (dates == null || rows == null || partCodes == null) { + return; + } + // Build groups preserving order + java.util.LinkedHashMap> groups = new java.util.LinkedHashMap<>(); + for (int i = 0; i < partCodes.length; i++) { + String code = partCodes[i]; + String grp = code != null && code.contains("_") ? code.split("_")[0] : code; + groups.computeIfAbsent(grp, k -> new java.util.ArrayList<>()).add(i); + } + + // Remove any single chart mode UI + removeAll(); + multiChartContainer = new javax.swing.JPanel(); + multiChartContainer.setLayout(new javax.swing.BoxLayout(multiChartContainer, javax.swing.BoxLayout.Y_AXIS)); + + // Color-blind friendly palette (ColorBrewer Set2-like) + java.awt.Color[] palette = new java.awt.Color[] { + new java.awt.Color(0x1b9e77), // green + new java.awt.Color(0xd95f02), // orange + new java.awt.Color(0x7570b3), // purple + new java.awt.Color(0xe7298a), // pink + new java.awt.Color(0x66a61e), // olive + new java.awt.Color(0xe6ab02), // mustard + new java.awt.Color(0xa6761d), // brown + new java.awt.Color(0x666666) // gray + }; + + for (var entry : groups.entrySet()) { + String grp = entry.getKey(); + java.util.List idxs = entry.getValue(); + org.jfree.data.time.TimeSeriesCollection dataset = new org.jfree.data.time.TimeSeriesCollection(); + + // For each skill in the group, build a time series across dates + for (int k = 0; k < idxs.size(); k++) { + int colIndex = idxs.get(k); + String code = partCodes[colIndex]; + String human = (partLabels != null && partLabels.length > colIndex && partLabels[colIndex] != null) ? partLabels[colIndex] : code; + String seriesName = code + " - " + human; // legend shows code plus friendly label + org.jfree.data.time.TimeSeries ts = new org.jfree.data.time.TimeSeries(seriesName); + for (int r = 0; r < rows.size(); r++) { + java.time.LocalDate d = dates.get(r); + java.util.List row = rows.get(r); + Integer vv = (colIndex < row.size()) ? row.get(colIndex) : null; + double val = (double) (vv == null ? 0 : vv); + org.jfree.data.time.Day day = new org.jfree.data.time.Day(java.util.Date.from(d.atStartOfDay(java.time.ZoneId.systemDefault()).toInstant())); + ts.addOrUpdate(day, addJitter(val)); + } + dataset.addSeries(ts); + } + + // Title: "Phase N Progression" when grp matches P + String title = (grp != null && grp.startsWith("P") && grp.length() > 1) + ? ("Phase " + grp.substring(1) + " Progression") + : (grp + " progression"); + + JFreeChart subchart = ChartFactory.createTimeSeriesChart( + title, + "Date", + "Value", + dataset, + true, + true, + false + ); + + XYPlot plot = subchart.getXYPlot(); + plot.setBackgroundPaint(java.awt.Color.WHITE); + // Add colored horizontal bands behind the data using polygon annotations + try { + // Compute domain lower/upper bounds in millis for the current dataset if available + long domainLower = Long.MIN_VALUE; + long domainUpper = Long.MAX_VALUE; + if (!dates.isEmpty()) { + java.time.ZoneId zid = java.time.ZoneId.systemDefault(); + java.time.LocalDate first = dates.get(0); + java.time.LocalDate last = dates.get(dates.size() - 1).plusDays(4); + domainLower = java.util.Date.from(first.atStartOfDay(zid).toInstant()).getTime(); + domainUpper = java.util.Date.from(last.atStartOfDay(zid).toInstant()).getTime(); + } + double left = domainLower == Long.MIN_VALUE ? plot.getDomainAxis().getRange().getLowerBound() : domainLower; + double right = domainUpper == Long.MAX_VALUE ? plot.getDomainAxis().getRange().getUpperBound() : domainUpper; + // Use shared helper to draw bands in the domain coordinates (millis) + addHorizontalBands(plot, left, right); + } catch (Throwable t) { + LOG.debug("Unable to add background bands as annotations: {}", t.toString()); + } + org.jfree.chart.renderer.xy.XYLineAndShapeRenderer renderer = new org.jfree.chart.renderer.xy.XYLineAndShapeRenderer(true, true); + // assign colors and markers + for (int s = 0; s < dataset.getSeriesCount(); s++) { + java.awt.Color c = palette[s % palette.length]; + renderer.setSeriesPaint(s, c); + renderer.setSeriesStroke(s, new java.awt.BasicStroke(2.0f)); + renderer.setSeriesShapesVisible(s, true); + renderer.setSeriesShape(s, new java.awt.geom.Ellipse2D.Double(-3, -3, 6, 6)); + } + plot.setRenderer(renderer); + + // Ensure Y axis range and ticks show 0..3 grid with a small lower padding for x-axis visibility + try { + NumberAxis yAxis = (NumberAxis) plot.getRangeAxis(); + yAxis.setRange(-0.25, 4.25); + yAxis.setTickUnit(new org.jfree.chart.axis.NumberTickUnit(1)); + } catch (ClassCastException cce) { + LOG.debug("Range axis is not a NumberAxis: {}", cce.toString()); + } + + // Ensure Y axis range and ticks show 0..3 grid with a small lower padding for x-axis visibility + try { + org.jfree.chart.axis.DateAxis dateAxis = (org.jfree.chart.axis.DateAxis) plot.getDomainAxis(); + java.text.SimpleDateFormat fmt = new java.text.SimpleDateFormat("yyyyMMdd"); + dateAxis.setDateFormatOverride(fmt); + // Use the provided dates list to determine bounds (oldest first) + if (!dates.isEmpty()) { + java.time.ZoneId zid = java.time.ZoneId.systemDefault(); + java.time.LocalDate firstDate = dates.get(0); + java.time.LocalDate lastDate = dates.get(dates.size() - 1); + // pad 4 days on the right to provide visual breathing room + java.time.LocalDate paddedUpper = lastDate.plusDays(4); + java.util.Date lower = java.util.Date.from(firstDate.atStartOfDay(zid).toInstant()); + java.util.Date upper = java.util.Date.from(paddedUpper.atStartOfDay(zid).toInstant()); + dateAxis.setRange(lower, upper); + // one-day tick units so each datapoint maps to a single label + dateAxis.setTickUnit(new org.jfree.chart.axis.DateTickUnit(org.jfree.chart.axis.DateTickUnitType.DAY, 1)); + } + } catch (ClassCastException cce) { + LOG.debug("Domain axis is not a DateAxis: {}", cce.toString()); + } + + // Place legend below the plot for clarity and allow it to show codes+labels + if (subchart.getLegend() != null) { + subchart.getLegend().setPosition(org.jfree.chart.ui.RectangleEdge.BOTTOM); + } + + ChartPanel cp = new ChartPanel(subchart); + cp.setName(grp); + cp.setPreferredSize(new Dimension(1000, Math.max(180, 40 * idxs.size()))); + cp.setMaximumSize(new Dimension(Integer.MAX_VALUE, cp.getPreferredSize().height)); + multiChartContainer.add(cp); + } + + add(new javax.swing.JScrollPane(multiChartContainer), BorderLayout.CENTER); + revalidate(); + repaint(); + } + + /** + * Save each grouped subchart as an individual PNG file. The method writes + * files named {baseName}-{group}.png into the provided directory and + * returns a map of group -> written path. Caller must ensure grouped data + * has been rendered (updateWithGroupedData called) prior to invoking this. + * + * @param dir directory to write files into + * @param baseName base filename (no extension) to prefix each file + * @param width image width in pixels + * @param heightPerGroup per-group image height in pixels + * @return ordered map of group id to written file path + * @throws java.io.IOException on I/O error + */ + public java.util.Map saveGroupedCharts(final java.nio.file.Path dir, final String baseName, final int width, final int heightPerGroup) throws java.io.IOException { + java.util.Map out = new java.util.LinkedHashMap<>(); + if (dir == null) { + throw new java.io.IOException("output dir is null"); + } + java.nio.file.Files.createDirectories(dir); + if (multiChartContainer == null || multiChartContainer.getComponentCount() == 0) { + return out; + } + for (int i = 0; i < multiChartContainer.getComponentCount(); i++) { + java.awt.Component c = multiChartContainer.getComponent(i); + String grp = c.getName() != null ? c.getName() : String.valueOf(i+1); + int h = Math.max(100, heightPerGroup); + c.setSize(width, h); + c.doLayout(); + java.awt.image.BufferedImage img = new java.awt.image.BufferedImage(width, h, java.awt.image.BufferedImage.TYPE_INT_ARGB); + java.awt.Graphics2D g = img.createGraphics(); + g.setColor(java.awt.Color.WHITE); + g.fillRect(0, 0, width, h); + c.paint(g); + g.dispose(); + java.nio.file.Path file = dir.resolve(baseName + "-" + grp + ".png"); + try (java.io.OutputStream os = java.nio.file.Files.newOutputStream(file); + javax.imageio.stream.ImageOutputStream ios = javax.imageio.ImageIO.createImageOutputStream(os)) { + boolean written = javax.imageio.ImageIO.write(img, "png", ios); + if (!written) { + throw new java.io.IOException("No ImageWriter for png"); + } + } + out.put(grp, file); + } + return out; + } + + /** + * Show an empty grouped chart using the provided part codes. This will + * render one row of zeros sized to the number of parts so the UI shows + * grouped axes and placeholders even when no session data exists yet. + * + * @param partCodes array of part codes used to determine the number of columns + */ + public void showEmptyGrouped(final String[] partCodes) { + if (partCodes == null) { + return; + } + List zeros = new java.util.ArrayList<>(java.util.Collections.nCopies(partCodes.length, 0)); + List> rows = new java.util.ArrayList<>(); + rows.add(zeros); + updateWithGroupedData(rows, partCodes); + } + + /** + * Save the current chart to a PNG file. If the chart is empty this will + * still export the rendered chart panel contents. + * + * @param outputPath path to write the PNG file to + * @param width image width in pixels + * @param height image height in pixels + * @throws java.io.IOException if writing fails + */ + public void saveChart(final java.nio.file.Path outputPath, final int width, final int height) throws java.io.IOException { + if (outputPath == null) { + throw new java.io.IOException("outputPath is null"); + } + java.nio.file.Path parent = outputPath.getParent(); + if (parent == null) { + parent = java.nio.file.Paths.get("."); + } + // Ensure parent directory exists + java.nio.file.Files.createDirectories(parent); + java.awt.image.BufferedImage img = null; + // If we are in grouped-chart mode, render the multiChartContainer component + if (multiChartContainer != null && multiChartContainer.getComponentCount() > 0) { + // Ensure layout sizes are applied + multiChartContainer.setSize(width, height); + multiChartContainer.doLayout(); + img = new java.awt.image.BufferedImage(width, height, java.awt.image.BufferedImage.TYPE_INT_ARGB); + java.awt.Graphics2D g = img.createGraphics(); + // paint background white to match chart look + g.setColor(java.awt.Color.WHITE); + g.fillRect(0, 0, width, height); + multiChartContainer.paint(g); + g.dispose(); + } else if (chart != null) { + img = chart.createBufferedImage(width, height); + } else { + throw new java.io.IOException("No chart available to render"); + } + + try { + // Use an explicit OutputStream -> ImageOutputStream to avoid platform-specific ImageIO issues + try (java.io.OutputStream os = java.nio.file.Files.newOutputStream(outputPath); + javax.imageio.stream.ImageOutputStream ios = javax.imageio.ImageIO.createImageOutputStream(os)) { + boolean written = javax.imageio.ImageIO.write(img, "png", ios); + if (!written) { + throw new java.io.IOException("No ImageWriter available for format 'png'"); + } + } + } catch (java.io.IOException ioe) { + String diag = String.format("Failed saving chart to %s (parentExists=%b, parentWritable=%b, parentIsDir=%b)", + outputPath.toString(), java.nio.file.Files.exists(parent), java.nio.file.Files.isWritable(parent), java.nio.file.Files.isDirectory(parent)); + throw new java.io.IOException(diag, ioe); + } + } + + private void updateXAxisLabels() { + // Generate labels for the X-axis + String[] skillLabels = new String[NUMBER_OF_SKILLS]; + int skillGroup = 1; + int skillNumber = 1; + for (int i = 0; i < NUMBER_OF_SKILLS; i++) { + skillLabels[i] = "Skill" + skillGroup + "-" + skillNumber; + skillNumber++; + if ((skillGroup == 1 && skillNumber > 6) || + (skillGroup == 2 && skillNumber > 4) || + (skillGroup == 3 && skillNumber > 11) || + (skillGroup == 4 && skillNumber > 7)) { + skillGroup++; + skillNumber = 1; + } + } + + // Set the custom labels on the X-axis + NumberAxis domain = (NumberAxis) chart.getXYPlot().getDomainAxis(); + domain.setVerticalTickLabels(true); + domain.setTickLabelFont(new Font("SansSerif", Font.PLAIN, 8)); + domain.setTickUnit(new org.jfree.chart.axis.NumberTickUnit(1) { + @Override + public String valueToString(double value) { + int index = (int) value - 1; + if (index >= 0 && index < skillLabels.length) { + return skillLabels[index]; + } + return ""; + } + }); + } +} diff --git a/src/main/java/com/studentgui/apppages/Keyboarding.java b/src/main/java/com/studentgui/apppages/Keyboarding.java index 143dfea..11252ec 100644 --- a/src/main/java/com/studentgui/apppages/Keyboarding.java +++ b/src/main/java/com/studentgui/apppages/Keyboarding.java @@ -1,238 +1,268 @@ -package com.studentgui.apppages; - -import java.awt.BorderLayout; -import java.awt.Dimension; -import java.awt.Font; -import java.awt.GridBagConstraints; -import java.awt.GridBagLayout; -import java.awt.Insets; -import java.awt.event.ActionEvent; -import java.awt.event.KeyEvent; -import java.sql.SQLException; -import java.time.LocalDate; - -import javax.swing.JButton; -import javax.swing.JLabel; -import javax.swing.JOptionPane; -import javax.swing.JPanel; -import javax.swing.JScrollPane; -import javax.swing.JTextField; -import javax.swing.SwingUtilities; - -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; - -/** - * Keyboarding skills page. Captures program/topic/speed/accuracy results and - * persists them to a dedicated keyboarding result table via the - * {@code Database} helper. - */ -public class Keyboarding extends JPanel implements com.studentgui.app.DateChangeListener, com.studentgui.app.StudentChangeListener { - private static final Logger LOG = LoggerFactory.getLogger(Keyboarding.class); - /** Text field for the program or curriculum name. */ - private final JTextField programField, topicField, speedField, accuracyField; - - /** Shared graph component (present but not used for keyboarding plotting). */ - private final JLineGraph lineGraph; - - /** Selected student's display name for saves/refreshes (may be null). */ - private String studentNameParam; - /** Page header label. */ - private JLabel titleLabel; - /** Base title text for the Keyboarding page; date suffix appended in UI. */ - private final String baseTitle = "Keyboarding Skills"; - - /** Session date associated with persisted keyboarding results. */ - private LocalDate dateParam; - - /** - * Construct the Keyboarding page for a specific student and session date. - * - * @param studentName selected student's display name (may be null) - * @param date session date used for persisted results - * @param lineGraph shared graph component (unused for keyboarding results) - */ - public Keyboarding(String studentName, LocalDate date, JLineGraph lineGraph) { - this.studentNameParam = (studentName == null || studentName.trim().isEmpty()) ? com.studentgui.apphelpers.Helpers.defaultStudent() : studentName; - this.dateParam = date; - this.lineGraph = lineGraph; - setLayout(new BorderLayout()); - - JPanel p = new JPanel(new GridBagLayout()); - JPanel view = new JPanel(new BorderLayout()); - view.add(p, BorderLayout.NORTH); - view.setBorder(javax.swing.BorderFactory.createEmptyBorder(20,20,20,20)); - JScrollPane scroll = new JScrollPane(view); - scroll.getAccessibleContext().setAccessibleName("Keyboarding data entry scroll pane"); - p.setBorder(javax.swing.BorderFactory.createEmptyBorder(20,20,20,20)); - GridBagConstraints gbc = new GridBagConstraints(); gbc.insets=new Insets(2,2,2,2); gbc.fill = GridBagConstraints.HORIZONTAL; gbc.anchor = GridBagConstraints.NORTHWEST; - this.titleLabel = new JLabel(baseTitle, JLabel.LEFT); - this.titleLabel.setFont(this.titleLabel.getFont().deriveFont(Font.BOLD,16)); - this.titleLabel.getAccessibleContext().setAccessibleName("Keyboarding Skills Title"); - gbc.gridx=0; gbc.gridy=0; gbc.gridwidth=2; p.add(this.titleLabel, gbc); - - gbc.gridwidth=1; - // Normalize label width to the PhaseScoreField global width so inputs align - int globalLabel = com.studentgui.uicomp.PhaseScoreField.getGlobalLabelWidth(); - gbc.gridy=1; gbc.gridx=0; JLabel programLabel = new JLabel("Program:"); programLabel.setPreferredSize(new Dimension(globalLabel, programLabel.getPreferredSize().height)); p.add(programLabel, gbc); gbc.gridx=1; programField = new JTextField(); programField.setPreferredSize(new Dimension(300,24)); programField.setToolTipText("Name of the program or curriculum"); programField.getAccessibleContext().setAccessibleName("Program"); p.add(programField, gbc); programLabel.setLabelFor(programField); - gbc.gridy=2; gbc.gridx=0; JLabel topicLabel = new JLabel("Topic:"); topicLabel.setPreferredSize(new Dimension(globalLabel, topicLabel.getPreferredSize().height)); p.add(topicLabel, gbc); gbc.gridx=1; topicField = new JTextField(); topicField.setPreferredSize(new Dimension(300,24)); topicField.setToolTipText("Topic or lesson name"); topicField.getAccessibleContext().setAccessibleName("Topic"); p.add(topicField, gbc); topicLabel.setLabelFor(topicField); - gbc.gridy=3; gbc.gridx=0; JLabel speedLabel = new JLabel("Speed (WPM):"); speedLabel.setPreferredSize(new Dimension(globalLabel, speedLabel.getPreferredSize().height)); p.add(speedLabel, gbc); gbc.gridx=1; speedField = new JTextField("0"); speedField.setPreferredSize(new Dimension(100,24)); speedField.setToolTipText("Words per minute"); speedField.getAccessibleContext().setAccessibleName("Speed (WPM)"); p.add(speedField, gbc); speedLabel.setLabelFor(speedField); - gbc.gridy=4; gbc.gridx=0; JLabel accuracyLabel = new JLabel("Accuracy (%):"); accuracyLabel.setPreferredSize(new Dimension(globalLabel, accuracyLabel.getPreferredSize().height)); p.add(accuracyLabel, gbc); gbc.gridx=1; accuracyField = new JTextField("0"); accuracyField.setPreferredSize(new Dimension(100,24)); accuracyField.setToolTipText("Accuracy percentage"); accuracyField.getAccessibleContext().setAccessibleName("Accuracy (%)"); p.add(accuracyField, gbc); accuracyLabel.setLabelFor(accuracyField); - - gbc.gridy=5; gbc.gridx=0; gbc.gridwidth=GridBagConstraints.REMAINDER; - JButton submit = new JButton("Submit Data"); - submit.setPreferredSize(new java.awt.Dimension(0, 32)); - submit.addActionListener((ActionEvent e)-> { submitData(); refreshGraph(); }); - submit.setToolTipText("Save keyboarding result for selected student"); - submit.setMnemonic(KeyEvent.VK_S); - submit.getAccessibleContext().setAccessibleName("Submit Keyboarding Data"); - p.add(submit, gbc); - gbc.gridwidth = 1; - // Removed separate Refresh Graph button; Submit Data now triggers refreshGraph - - add(scroll, BorderLayout.CENTER); - add(this.lineGraph, BorderLayout.SOUTH); - - SwingUtilities.invokeLater(()->{ p.setPreferredSize(p.getPreferredSize()); updateTitleDate(); revalidate(); }); - - com.studentgui.apphelpers.Helpers.createFolderHierarchy(); - initDatabase(); - refreshGraph(); - } - - /** - * Ensure the keyboarding progress type exists in the canonical schema. - */ - private void initDatabase() { - try { - com.studentgui.apphelpers.Database.getOrCreateProgressType("Keyboarding"); - } catch (SQLException ex) { - LOG.error("Error ensuring Keyboarding progress type", ex); - } - } - - /** - * Validate keyboarding inputs (speed and accuracy as integers) and - * persist a keyboarding result record for the selected student. - */ - private void submitData() { - if (this.studentNameParam == null || this.studentNameParam.trim().isEmpty()) { - JOptionPane.showMessageDialog(this, "Please select a student before saving keyboarding data.", "Missing student", JOptionPane.WARNING_MESSAGE); - return; - } - - try { - int studentId = com.studentgui.apphelpers.Database.getOrCreateStudent(this.studentNameParam); - int ptId = com.studentgui.apphelpers.Database.getOrCreateProgressType("Keyboarding"); - int sessionId = com.studentgui.apphelpers.Database.createProgressSession(studentId, ptId, this.dateParam); - - String program = programField.getText().trim(); - String topic = topicField.getText().trim(); - int speed; - int accuracy; - try { - String sp = speedField.getText().trim(); speed = sp.isEmpty() ? 0 : Integer.parseInt(sp); - } catch (NumberFormatException nfe) { - JOptionPane.showMessageDialog(this, "Please enter a whole number for Speed (WPM)", "Invalid input", JOptionPane.ERROR_MESSAGE); - speedField.requestFocusInWindow(); - return; - } - try { - String ac = accuracyField.getText().trim(); accuracy = ac.isEmpty() ? 0 : Integer.parseInt(ac); - } catch (NumberFormatException nfe) { - JOptionPane.showMessageDialog(this, "Please enter a whole number for Accuracy (%)", "Invalid input", JOptionPane.ERROR_MESSAGE); - accuracyField.requestFocusInWindow(); - return; - } - - com.studentgui.apphelpers.Database.insertKeyboardingResult(sessionId, program, topic, speed, accuracy); - LOG.info("Keyboarding data saved for {}", this.studentNameParam); - com.studentgui.apphelpers.UiNotifier.show("Keyboarding data saved."); - com.studentgui.apphelpers.dto.KeyboardingPayload payload = new com.studentgui.apphelpers.dto.KeyboardingPayload(sessionId, program, topic, speed, accuracy); - java.nio.file.Path jsonOut = com.studentgui.apphelpers.SessionJsonWriter.writeSessionJson(this.studentNameParam, "Keyboarding", payload, sessionId); - if (jsonOut == null) { - LOG.warn("Unable to save Keyboarding session JSON for sessionId={}", sessionId); - } - try { - java.nio.file.Path plotsOut = com.studentgui.apphelpers.Helpers.studentPlotsDir(this.studentNameParam); - java.nio.file.Path reportsOut = com.studentgui.apphelpers.Helpers.studentReportsDir(this.studentNameParam); - java.nio.file.Files.createDirectories(plotsOut); - java.nio.file.Files.createDirectories(reportsOut); - java.time.format.DateTimeFormatter df = java.time.format.DateTimeFormatter.ISO_DATE; - String dateStr = this.dateParam != null ? this.dateParam.format(df) : java.time.LocalDate.now().toString(); - String baseName = "Keyboarding-" + sessionId + "-" + dateStr; - - // Keyboarding doesn't have grouped codes; produce a small HTML/MD with metadata - StringBuilder md = new StringBuilder(); - md.append("# ").append(this.studentNameParam == null ? "Unknown Student" : this.studentNameParam).append(" - ").append(dateStr).append("\n\n"); - md.append("**Program:** ").append(program == null || program.isEmpty() ? "(none)" : program).append(" \n\n"); - md.append("**Topic:** ").append(topic == null || topic.isEmpty() ? "(none)" : topic).append(" \n\n"); - md.append("**Speed (WPM):** ").append(String.valueOf(speed)).append(" \n\n"); - md.append("**Accuracy (%):** ").append(String.valueOf(accuracy)).append(" \n\n"); - java.nio.file.Path mdFile = reportsOut.resolve(baseName + ".md"); - java.nio.file.Files.writeString(mdFile, md.toString(), java.nio.charset.StandardCharsets.UTF_8); - - try { - StringBuilder html = new StringBuilder(); - html.append(""); - html.append(this.studentNameParam == null ? "Student Report" : this.studentNameParam).append(" - ").append(dateStr).append(""); - html.append(""); - html.append(""); - html.append("

").append(this.studentNameParam == null ? "Unknown Student" : this.studentNameParam).append(" - ").append(dateStr).append("

"); - html.append("
\n"); - html.append("

Program: ").append(program == null || program.isEmpty() ? "(none)" : program).append("

"); - html.append("

Topic: ").append(topic == null || topic.isEmpty() ? "(none)" : topic).append("

"); - html.append("

Speed (WPM): ").append(String.valueOf(speed)).append("

"); - html.append("

Accuracy (%): ").append(String.valueOf(accuracy)).append("

"); - html.append("
"); - html.append(""); - java.nio.file.Path htmlFile = reportsOut.resolve(baseName + ".html"); - java.nio.file.Files.writeString(htmlFile, html.toString(), java.nio.charset.StandardCharsets.UTF_8); - LOG.info("Wrote Keyboarding session report {}", htmlFile); - } catch (java.io.IOException ioex) { - LOG.warn("Unable to write Keyboarding HTML report: {}", ioex.toString()); - } - } catch (java.io.IOException ioe) { - LOG.warn("Unable to save Keyboarding report: {}", ioe.toString()); - } - } catch (SQLException ex) { - LOG.error("DB error saving keyboarding data", ex); - JOptionPane.showMessageDialog(this, "Database error saving keyboarding data: " + ex.getMessage(), "Database error", JOptionPane.ERROR_MESSAGE); - } - } - - /** - * Refresh the keyboarding visualization. Currently keyboarding results are - * stored in a separate table and this method logs the request. - */ - private void refreshGraph() { - LOG.info("Keyboarding refresh requested for {}", studentNameParam); - } - - @Override - public void dateChanged(LocalDate newDate) { - this.dateParam = newDate; - SwingUtilities.invokeLater(() -> { - refreshGraph(); - updateTitleDate(); - }); - } - - @Override - public void studentChanged(String newStudent) { - this.studentNameParam = newStudent; - SwingUtilities.invokeLater(() -> { - refreshGraph(); - updateTitleDate(); - }); - } - - private void updateTitleDate() { - try { - String dateStr = this.dateParam != null ? this.dateParam.toString() : java.time.LocalDate.now().toString(); - this.titleLabel.setText(baseTitle + " - " + dateStr); - } catch (Exception ex) { - this.titleLabel.setText(baseTitle); - } - } -} +package com.studentgui.apppages; + +import java.awt.BorderLayout; +import java.awt.Dimension; +import java.awt.Font; +import java.awt.GridBagConstraints; +import java.awt.GridBagLayout; +import java.awt.Insets; +import java.awt.event.ActionEvent; +import java.awt.event.KeyEvent; +import java.sql.SQLException; +import java.time.LocalDate; + +import javax.swing.JButton; +import javax.swing.JLabel; +import javax.swing.JOptionPane; +import javax.swing.JPanel; +import javax.swing.JScrollPane; +import javax.swing.JTextField; +import javax.swing.SwingUtilities; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + * Touch-typing and keyboarding skills assessment page. + * + *

Unlike other assessment pages that use phase-score grids, this page captures + * structured performance metrics for keyboarding practice sessions:

+ * + *
    + *
  • Program: Name of the typing curriculum or software (e.g., TypingClub, KeyBlaze, Braille2000)
  • + *
  • Topic: Specific lesson, module, or exercise completed (e.g., "Home Row Mastery", "Lesson 12")
  • + *
  • Speed (WPM): Words per minute achieved during the timed exercise
  • + *
  • Accuracy (%): Percentage of characters typed correctly
  • + *
+ * + *

Data Persistence:

+ *
    + *
  • Values persisted via {@link com.studentgui.apphelpers.Database#insertKeyboardingResult} to the {@code KeyboardingResult} table
  • + *
  • JSON export: {@code StudentDataFiles//Sessions/Keyboarding/Keyboarding--.json}
  • + *
  • Metadata-only reports (no plots): Markdown and HTML files in {@code reports/} with session details
  • + *
+ * + *

Validation and Error Handling:

+ *
    + *
  • Speed and Accuracy fields must contain whole numbers (non-negative integers)
  • + *
  • Empty speed/accuracy fields default to 0 for leniency
  • + *
  • Invalid input triggers error dialogs and field focus for correction
  • + *
+ * + *

The shared {@link JLineGraph} component is present for UI consistency but is not populated + * with keyboarding data (keyboarding does not use assessment parts). Implements + * {@link com.studentgui.app.DateChangeListener} and {@link com.studentgui.app.StudentChangeListener} + * for title updates when global selections change.

+ * + * @see com.studentgui.apphelpers.Database#insertKeyboardingResult + * @see com.studentgui.apphelpers.dto.KeyboardingPayload + */ +public class Keyboarding extends JPanel implements com.studentgui.app.DateChangeListener, com.studentgui.app.StudentChangeListener { + private static final Logger LOG = LoggerFactory.getLogger(Keyboarding.class); + /** Text field for the program or curriculum name. */ + private final JTextField programField, topicField, speedField, accuracyField; + + /** Shared graph component (present but not used for keyboarding plotting). */ + private final JLineGraph lineGraph; + + /** Selected student's display name for saves/refreshes (may be null). */ + private String studentNameParam; + /** Page header label. */ + private JLabel titleLabel; + /** Base title text for the Keyboarding page; date suffix appended in UI. */ + private final String baseTitle = "Keyboarding Skills"; + + /** Session date associated with persisted keyboarding results. */ + private LocalDate dateParam; + + /** + * Construct the Keyboarding page for a specific student and session date. + * + * @param studentName selected student's display name (may be null) + * @param date session date used for persisted results + * @param lineGraph shared graph component (unused for keyboarding results) + */ + public Keyboarding(String studentName, LocalDate date, JLineGraph lineGraph) { + this.studentNameParam = (studentName == null || studentName.trim().isEmpty()) ? com.studentgui.apphelpers.Helpers.defaultStudent() : studentName; + this.dateParam = date; + this.lineGraph = lineGraph; + setLayout(new BorderLayout()); + + JPanel p = new JPanel(new GridBagLayout()); + JPanel view = new JPanel(new BorderLayout()); + view.add(p, BorderLayout.NORTH); + view.setBorder(javax.swing.BorderFactory.createEmptyBorder(20,20,20,20)); + JScrollPane scroll = new JScrollPane(view); + scroll.getAccessibleContext().setAccessibleName("Keyboarding data entry scroll pane"); + p.setBorder(javax.swing.BorderFactory.createEmptyBorder(20,20,20,20)); + GridBagConstraints gbc = new GridBagConstraints(); gbc.insets=new Insets(2,2,2,2); gbc.fill = GridBagConstraints.HORIZONTAL; gbc.anchor = GridBagConstraints.NORTHWEST; + this.titleLabel = new JLabel(baseTitle, JLabel.LEFT); + this.titleLabel.setFont(this.titleLabel.getFont().deriveFont(Font.BOLD,16)); + this.titleLabel.getAccessibleContext().setAccessibleName("Keyboarding Skills Title"); + gbc.gridx=0; gbc.gridy=0; gbc.gridwidth=2; p.add(this.titleLabel, gbc); + + gbc.gridwidth=1; + // Normalize label width to the PhaseScoreField global width so inputs align + int globalLabel = com.studentgui.uicomp.PhaseScoreField.getGlobalLabelWidth(); + gbc.gridy=1; gbc.gridx=0; JLabel programLabel = new JLabel("Program:"); programLabel.setPreferredSize(new Dimension(globalLabel, programLabel.getPreferredSize().height)); p.add(programLabel, gbc); gbc.gridx=1; programField = new JTextField(); programField.setPreferredSize(new Dimension(300,24)); programField.setToolTipText("Name of the program or curriculum"); programField.getAccessibleContext().setAccessibleName("Program"); p.add(programField, gbc); programLabel.setLabelFor(programField); + gbc.gridy=2; gbc.gridx=0; JLabel topicLabel = new JLabel("Topic:"); topicLabel.setPreferredSize(new Dimension(globalLabel, topicLabel.getPreferredSize().height)); p.add(topicLabel, gbc); gbc.gridx=1; topicField = new JTextField(); topicField.setPreferredSize(new Dimension(300,24)); topicField.setToolTipText("Topic or lesson name"); topicField.getAccessibleContext().setAccessibleName("Topic"); p.add(topicField, gbc); topicLabel.setLabelFor(topicField); + gbc.gridy=3; gbc.gridx=0; JLabel speedLabel = new JLabel("Speed (WPM):"); speedLabel.setPreferredSize(new Dimension(globalLabel, speedLabel.getPreferredSize().height)); p.add(speedLabel, gbc); gbc.gridx=1; speedField = new JTextField("0"); speedField.setPreferredSize(new Dimension(100,24)); speedField.setToolTipText("Words per minute"); speedField.getAccessibleContext().setAccessibleName("Speed (WPM)"); p.add(speedField, gbc); speedLabel.setLabelFor(speedField); + gbc.gridy=4; gbc.gridx=0; JLabel accuracyLabel = new JLabel("Accuracy (%):"); accuracyLabel.setPreferredSize(new Dimension(globalLabel, accuracyLabel.getPreferredSize().height)); p.add(accuracyLabel, gbc); gbc.gridx=1; accuracyField = new JTextField("0"); accuracyField.setPreferredSize(new Dimension(100,24)); accuracyField.setToolTipText("Accuracy percentage"); accuracyField.getAccessibleContext().setAccessibleName("Accuracy (%)"); p.add(accuracyField, gbc); accuracyLabel.setLabelFor(accuracyField); + + gbc.gridy=5; gbc.gridx=0; gbc.gridwidth=GridBagConstraints.REMAINDER; + JButton submit = new JButton("Submit Data"); + submit.setPreferredSize(new java.awt.Dimension(0, 32)); + submit.addActionListener((ActionEvent e)-> { submitData(); refreshGraph(); }); + submit.setToolTipText("Save keyboarding result for selected student"); + submit.setMnemonic(KeyEvent.VK_S); + submit.getAccessibleContext().setAccessibleName("Submit Keyboarding Data"); + p.add(submit, gbc); + gbc.gridwidth = 1; + // Removed separate Refresh Graph button; Submit Data now triggers refreshGraph + + add(scroll, BorderLayout.CENTER); + add(this.lineGraph, BorderLayout.SOUTH); + + SwingUtilities.invokeLater(()->{ p.setPreferredSize(p.getPreferredSize()); updateTitleDate(); revalidate(); }); + + com.studentgui.apphelpers.Helpers.createFolderHierarchy(); + initDatabase(); + refreshGraph(); + } + + /** + * Ensure the keyboarding progress type exists in the canonical schema. + */ + private void initDatabase() { + try { + com.studentgui.apphelpers.Database.getOrCreateProgressType("Keyboarding"); + } catch (SQLException ex) { + LOG.error("Error ensuring Keyboarding progress type", ex); + } + } + + /** + * Validate keyboarding inputs (speed and accuracy as integers) and + * persist a keyboarding result record for the selected student. + */ + private void submitData() { + if (this.studentNameParam == null || this.studentNameParam.trim().isEmpty()) { + JOptionPane.showMessageDialog(this, "Please select a student before saving keyboarding data.", "Missing student", JOptionPane.WARNING_MESSAGE); + return; + } + + try { + int studentId = com.studentgui.apphelpers.Database.getOrCreateStudent(this.studentNameParam); + int ptId = com.studentgui.apphelpers.Database.getOrCreateProgressType("Keyboarding"); + int sessionId = com.studentgui.apphelpers.Database.createProgressSession(studentId, ptId, this.dateParam); + + String program = programField.getText().trim(); + String topic = topicField.getText().trim(); + int speed; + int accuracy; + try { + String sp = speedField.getText().trim(); speed = sp.isEmpty() ? 0 : Integer.parseInt(sp); + } catch (NumberFormatException nfe) { + JOptionPane.showMessageDialog(this, "Please enter a whole number for Speed (WPM)", "Invalid input", JOptionPane.ERROR_MESSAGE); + speedField.requestFocusInWindow(); + return; + } + try { + String ac = accuracyField.getText().trim(); accuracy = ac.isEmpty() ? 0 : Integer.parseInt(ac); + } catch (NumberFormatException nfe) { + JOptionPane.showMessageDialog(this, "Please enter a whole number for Accuracy (%)", "Invalid input", JOptionPane.ERROR_MESSAGE); + accuracyField.requestFocusInWindow(); + return; + } + + com.studentgui.apphelpers.Database.insertKeyboardingResult(sessionId, program, topic, speed, accuracy); + LOG.info("Keyboarding data saved for {}", this.studentNameParam); + com.studentgui.apphelpers.UiNotifier.show("Keyboarding data saved."); + com.studentgui.apphelpers.dto.KeyboardingPayload payload = new com.studentgui.apphelpers.dto.KeyboardingPayload(sessionId, program, topic, speed, accuracy); + java.nio.file.Path jsonOut = com.studentgui.apphelpers.SessionJsonWriter.writeSessionJson(this.studentNameParam, "Keyboarding", payload, sessionId); + if (jsonOut == null) { + LOG.warn("Unable to save Keyboarding session JSON for sessionId={}", sessionId); + } + try { + java.nio.file.Path plotsOut = com.studentgui.apphelpers.Helpers.studentPlotsDir(this.studentNameParam); + java.nio.file.Path reportsOut = com.studentgui.apphelpers.Helpers.studentReportsDir(this.studentNameParam); + java.nio.file.Files.createDirectories(plotsOut); + java.nio.file.Files.createDirectories(reportsOut); + java.time.format.DateTimeFormatter df = java.time.format.DateTimeFormatter.ISO_DATE; + String dateStr = this.dateParam != null ? this.dateParam.format(df) : java.time.LocalDate.now().toString(); + String baseName = "Keyboarding-" + sessionId + "-" + dateStr; + + // Keyboarding doesn't have grouped codes; produce a small HTML/MD with metadata + StringBuilder md = new StringBuilder(); + md.append("# ").append(this.studentNameParam == null ? "Unknown Student" : this.studentNameParam).append(" - ").append(dateStr).append("\n\n"); + md.append("**Program:** ").append(program == null || program.isEmpty() ? "(none)" : program).append(" \n\n"); + md.append("**Topic:** ").append(topic == null || topic.isEmpty() ? "(none)" : topic).append(" \n\n"); + md.append("**Speed (WPM):** ").append(String.valueOf(speed)).append(" \n\n"); + md.append("**Accuracy (%):** ").append(String.valueOf(accuracy)).append(" \n\n"); + java.nio.file.Path mdFile = reportsOut.resolve(baseName + ".md"); + java.nio.file.Files.writeString(mdFile, md.toString(), java.nio.charset.StandardCharsets.UTF_8); + + try { + StringBuilder html = new StringBuilder(); + html.append(""); + html.append(this.studentNameParam == null ? "Student Report" : this.studentNameParam).append(" - ").append(dateStr).append(""); + html.append(""); + html.append(""); + html.append("

").append(this.studentNameParam == null ? "Unknown Student" : this.studentNameParam).append(" - ").append(dateStr).append("

"); + html.append("
\n"); + html.append("

Program: ").append(program == null || program.isEmpty() ? "(none)" : program).append("

"); + html.append("

Topic: ").append(topic == null || topic.isEmpty() ? "(none)" : topic).append("

"); + html.append("

Speed (WPM): ").append(String.valueOf(speed)).append("

"); + html.append("

Accuracy (%): ").append(String.valueOf(accuracy)).append("

"); + html.append("
"); + html.append(""); + java.nio.file.Path htmlFile = reportsOut.resolve(baseName + ".html"); + java.nio.file.Files.writeString(htmlFile, html.toString(), java.nio.charset.StandardCharsets.UTF_8); + LOG.info("Wrote Keyboarding session report {}", htmlFile); + } catch (java.io.IOException ioex) { + LOG.warn("Unable to write Keyboarding HTML report: {}", ioex.toString()); + } + } catch (java.io.IOException ioe) { + LOG.warn("Unable to save Keyboarding report: {}", ioe.toString()); + } + } catch (SQLException ex) { + LOG.error("DB error saving keyboarding data", ex); + JOptionPane.showMessageDialog(this, "Database error saving keyboarding data: " + ex.getMessage(), "Database error", JOptionPane.ERROR_MESSAGE); + } + } + + /** + * Refresh the keyboarding visualization. Currently keyboarding results are + * stored in a separate table and this method logs the request. + */ + private void refreshGraph() { + LOG.info("Keyboarding refresh requested for {}", studentNameParam); + } + + @Override + public void dateChanged(LocalDate newDate) { + this.dateParam = newDate; + SwingUtilities.invokeLater(() -> { + refreshGraph(); + updateTitleDate(); + }); + } + + @Override + public void studentChanged(String newStudent) { + this.studentNameParam = newStudent; + SwingUtilities.invokeLater(() -> { + refreshGraph(); + updateTitleDate(); + }); + } + + private void updateTitleDate() { + try { + String dateStr = this.dateParam != null ? this.dateParam.toString() : java.time.LocalDate.now().toString(); + this.titleLabel.setText(baseTitle + " - " + dateStr); + } catch (Exception ex) { + this.titleLabel.setText(baseTitle); + } + } +} diff --git a/src/main/java/com/studentgui/apppages/Observations.java b/src/main/java/com/studentgui/apppages/Observations.java index fff60ee..c5ce19d 100644 --- a/src/main/java/com/studentgui/apppages/Observations.java +++ b/src/main/java/com/studentgui/apppages/Observations.java @@ -1,118 +1,145 @@ -package com.studentgui.apppages; - -import java.awt.BorderLayout; -import java.awt.Font; -import java.awt.GridBagConstraints; -import java.awt.GridBagLayout; -import java.awt.Insets; -import java.awt.event.ActionEvent; -import java.awt.event.KeyEvent; -import java.sql.SQLException; -import java.time.LocalDate; - -import javax.swing.JButton; -import javax.swing.JLabel; -import javax.swing.JOptionPane; -import javax.swing.JPanel; -import javax.swing.JScrollPane; -import javax.swing.JTextArea; -import javax.swing.SwingUtilities; - -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; - -/** - * Observations page for recording freeform observational notes. - */ -public class Observations extends JPanel { - private static final Logger LOG = LoggerFactory.getLogger(Observations.class); - /** Multi-line text area for entering observational notes. */ - private final JTextArea notesArea; - - /** Selected student's display name (may be null) for this observation session. */ - private final String studentNameParam; - - /** Date associated with the recorded observations. */ - private final LocalDate dateParam; - - /** - * Create an Observations page for the given student and date. - * - * @param studentName student display name (may be null when no student selected) - * @param date the date this observation applies to - */ - public Observations(String studentName, LocalDate date) { - this.studentNameParam = (studentName == null || studentName.trim().isEmpty()) ? com.studentgui.apphelpers.Helpers.defaultStudent() : studentName; - this.dateParam = date; - setLayout(new BorderLayout()); - - JPanel p = new JPanel(new GridBagLayout()); - JPanel view = new JPanel(new BorderLayout()); - view.add(p, BorderLayout.NORTH); - view.setBorder(javax.swing.BorderFactory.createEmptyBorder(20,20,20,20)); - JScrollPane scroll = new JScrollPane(view); - scroll.getAccessibleContext().setAccessibleName("Observations data entry scroll pane"); - GridBagConstraints gbc = new GridBagConstraints(); gbc.insets=new Insets(2,2,2,2); gbc.fill = GridBagConstraints.BOTH; gbc.anchor = GridBagConstraints.NORTHWEST; - JLabel title = new JLabel("Observations", JLabel.LEFT); - title.setFont(title.getFont().deriveFont(Font.BOLD,16)); - title.getAccessibleContext().setAccessibleName("Observations Title"); - gbc.gridx=0; gbc.gridy=0; gbc.gridwidth=1; p.add(title, gbc); - - gbc.gridy=1; gbc.gridx=0; JLabel notesLabel = new JLabel("Notes:"); p.add(notesLabel, gbc); - gbc.gridy=2; gbc.gridx=0; notesArea = new JTextArea(8,40); notesArea.setLineWrap(true); notesArea.setWrapStyleWord(true); notesArea.setToolTipText("Enter observational notes for the student"); notesArea.getAccessibleContext().setAccessibleName("Observations notes"); p.add(notesArea, gbc); - notesLabel.setLabelFor(notesArea); - - // Filler so the scroll content has room and the form is visible (prevents - // the shared graph in SOUTH from visually dominating the view) - gbc.gridy = 3; gbc.gridx = 0; gbc.gridwidth = GridBagConstraints.REMAINDER; gbc.weighty = 1.0; - p.add(new JPanel(), gbc); - gbc.weighty = 0.0; gbc.gridwidth = 1; - - gbc.gridy = 4; JButton submit = new JButton("Save Notes"); - submit.addActionListener((ActionEvent e)-> saveNotes()); - submit.setMnemonic(KeyEvent.VK_S); - submit.setToolTipText("Save observational notes (Alt+S)"); - submit.getAccessibleContext().setAccessibleName("Save Observations Notes"); - gbc.gridx = 0; gbc.anchor = GridBagConstraints.WEST; - p.add(submit, gbc); - // consume remaining columns so layout stays consistent - gbc.gridx = 1; gbc.gridwidth = GridBagConstraints.REMAINDER; p.add(new JPanel(), gbc); - - add(scroll, BorderLayout.CENTER); - - SwingUtilities.invokeLater(()->{ p.setPreferredSize(p.getPreferredSize()); revalidate(); }); - - com.studentgui.apphelpers.Helpers.createFolderHierarchy(); - } - - /** - * Persist the contents of the notes area into the canonical database. - * Creates or re-uses the student, progress type and session records as needed. - */ - private void saveNotes() { - if (this.studentNameParam == null || this.studentNameParam.trim().isEmpty()) { - JOptionPane.showMessageDialog(this, "Please select a student before saving observations.", "Missing student", JOptionPane.WARNING_MESSAGE); - return; - } - - try { - int studentId = com.studentgui.apphelpers.Database.getOrCreateStudent(this.studentNameParam); - int ptId = com.studentgui.apphelpers.Database.getOrCreateProgressType("Observations"); - int sessionId = com.studentgui.apphelpers.Database.createProgressSession(studentId, ptId, this.dateParam); - String notes = notesArea.getText(); - com.studentgui.apphelpers.Database.insertAssessmentResults(sessionId, ptId, new String[]{"OBS_NOTE"}, new int[]{0}); - // store the notes in the ProgressSession.notes column via helper - com.studentgui.apphelpers.Database.saveSessionNotes(sessionId, notes); - LOG.info("Saved observations for {}", studentNameParam); - com.studentgui.apphelpers.UiNotifier.show("Observations saved."); - com.studentgui.apphelpers.dto.NotesPayload payload = new com.studentgui.apphelpers.dto.NotesPayload(sessionId, notes); - java.nio.file.Path jsonOut = com.studentgui.apphelpers.SessionJsonWriter.writeSessionJson(this.studentNameParam, "Observations", payload, sessionId); - if (jsonOut == null) { - LOG.warn("Unable to save Observations session JSON for sessionId={}", sessionId); - } - } catch (SQLException ex) { - LOG.error("Error saving observations", ex); - JOptionPane.showMessageDialog(this, "Database error saving observations: " + ex.getMessage(), "Database error", JOptionPane.ERROR_MESSAGE); - } - } -} +package com.studentgui.apppages; + +import java.awt.BorderLayout; +import java.awt.Font; +import java.awt.GridBagConstraints; +import java.awt.GridBagLayout; +import java.awt.Insets; +import java.awt.event.ActionEvent; +import java.awt.event.KeyEvent; +import java.sql.SQLException; +import java.time.LocalDate; + +import javax.swing.JButton; +import javax.swing.JLabel; +import javax.swing.JOptionPane; +import javax.swing.JPanel; +import javax.swing.JScrollPane; +import javax.swing.JTextArea; +import javax.swing.SwingUtilities; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + * Observational notes page for documenting unstructured student behaviors and progress. + * + *

Similar to {@link SessionNotes} but intended for ongoing observational records rather than + * post-session reflections. Provides a multi-line text area for educators to capture qualitative + * observations throughout or across multiple sessions.

+ * + *

Typical Use Cases:

+ *
    + *
  • Recording specific skill demonstrations observed in real-time (e.g., "Student independently located Braille cell for letter 'G' after 2 attempts")
  • + *
  • Documenting spontaneous behaviors or breakthroughs (e.g., "First time student used VoiceOver gestures without prompting")
  • + *
  • Noting patterns over time (e.g., "Third session this week where student requested breaks during Abacus work")
  • + *
  • Functional vision assessments and CVI-related observations
  • + *
+ * + *

Data Persistence:

+ *
    + *
  • Notes saved via {@link com.studentgui.apphelpers.Database#saveSessionNotes} to {@code ProgressSession.notes} column
  • + *
  • Associated with an Observations progress type for categorization
  • + *
  • Dummy assessment result (code="OBS_NOTE", score=0) inserted to satisfy schema constraints
  • + *
  • JSON export: {@code StudentDataFiles//Sessions/Observations/Observations--.json}
  • + *
+ * + *

No plots or quantitative reports are generated. This page does not implement listener interfaces + * and operates on static student/date parameters set at construction time.

+ * + * @see com.studentgui.apphelpers.Database#saveSessionNotes + * @see com.studentgui.apphelpers.dto.NotesPayload + * @see SessionNotes + */ +public class Observations extends JPanel { + private static final Logger LOG = LoggerFactory.getLogger(Observations.class); + /** Multi-line text area for entering observational notes. */ + private final JTextArea notesArea; + + /** Selected student's display name (may be null) for this observation session. */ + private final String studentNameParam; + + /** Date associated with the recorded observations. */ + private final LocalDate dateParam; + + /** + * Create an Observations page for the given student and date. + * + * @param studentName student display name (may be null when no student selected) + * @param date the date this observation applies to + */ + public Observations(String studentName, LocalDate date) { + this.studentNameParam = (studentName == null || studentName.trim().isEmpty()) ? com.studentgui.apphelpers.Helpers.defaultStudent() : studentName; + this.dateParam = date; + setLayout(new BorderLayout()); + + JPanel p = new JPanel(new GridBagLayout()); + JPanel view = new JPanel(new BorderLayout()); + view.add(p, BorderLayout.NORTH); + view.setBorder(javax.swing.BorderFactory.createEmptyBorder(20,20,20,20)); + JScrollPane scroll = new JScrollPane(view); + scroll.getAccessibleContext().setAccessibleName("Observations data entry scroll pane"); + GridBagConstraints gbc = new GridBagConstraints(); gbc.insets=new Insets(2,2,2,2); gbc.fill = GridBagConstraints.BOTH; gbc.anchor = GridBagConstraints.NORTHWEST; + JLabel title = new JLabel("Observations", JLabel.LEFT); + title.setFont(title.getFont().deriveFont(Font.BOLD,16)); + title.getAccessibleContext().setAccessibleName("Observations Title"); + gbc.gridx=0; gbc.gridy=0; gbc.gridwidth=1; p.add(title, gbc); + + gbc.gridy=1; gbc.gridx=0; JLabel notesLabel = new JLabel("Notes:"); p.add(notesLabel, gbc); + gbc.gridy=2; gbc.gridx=0; notesArea = new JTextArea(8,40); notesArea.setLineWrap(true); notesArea.setWrapStyleWord(true); notesArea.setToolTipText("Enter observational notes for the student"); notesArea.getAccessibleContext().setAccessibleName("Observations notes"); p.add(notesArea, gbc); + notesLabel.setLabelFor(notesArea); + + // Filler so the scroll content has room and the form is visible (prevents + // the shared graph in SOUTH from visually dominating the view) + gbc.gridy = 3; gbc.gridx = 0; gbc.gridwidth = GridBagConstraints.REMAINDER; gbc.weighty = 1.0; + p.add(new JPanel(), gbc); + gbc.weighty = 0.0; gbc.gridwidth = 1; + + gbc.gridy = 4; JButton submit = new JButton("Save Notes"); + submit.addActionListener((ActionEvent e)-> saveNotes()); + submit.setMnemonic(KeyEvent.VK_S); + submit.setToolTipText("Save observational notes (Alt+S)"); + submit.getAccessibleContext().setAccessibleName("Save Observations Notes"); + gbc.gridx = 0; gbc.anchor = GridBagConstraints.WEST; + p.add(submit, gbc); + // consume remaining columns so layout stays consistent + gbc.gridx = 1; gbc.gridwidth = GridBagConstraints.REMAINDER; p.add(new JPanel(), gbc); + + add(scroll, BorderLayout.CENTER); + + SwingUtilities.invokeLater(()->{ p.setPreferredSize(p.getPreferredSize()); revalidate(); }); + + com.studentgui.apphelpers.Helpers.createFolderHierarchy(); + } + + /** + * Persist the contents of the notes area into the canonical database. + * Creates or re-uses the student, progress type and session records as needed. + */ + private void saveNotes() { + if (this.studentNameParam == null || this.studentNameParam.trim().isEmpty()) { + JOptionPane.showMessageDialog(this, "Please select a student before saving observations.", "Missing student", JOptionPane.WARNING_MESSAGE); + return; + } + + try { + int studentId = com.studentgui.apphelpers.Database.getOrCreateStudent(this.studentNameParam); + int ptId = com.studentgui.apphelpers.Database.getOrCreateProgressType("Observations"); + int sessionId = com.studentgui.apphelpers.Database.createProgressSession(studentId, ptId, this.dateParam); + String notes = notesArea.getText(); + com.studentgui.apphelpers.Database.insertAssessmentResults(sessionId, ptId, new String[]{"OBS_NOTE"}, new int[]{0}); + // store the notes in the ProgressSession.notes column via helper + com.studentgui.apphelpers.Database.saveSessionNotes(sessionId, notes); + LOG.info("Saved observations for {}", studentNameParam); + com.studentgui.apphelpers.UiNotifier.show("Observations saved."); + com.studentgui.apphelpers.dto.NotesPayload payload = new com.studentgui.apphelpers.dto.NotesPayload(sessionId, notes); + java.nio.file.Path jsonOut = com.studentgui.apphelpers.SessionJsonWriter.writeSessionJson(this.studentNameParam, "Observations", payload, sessionId); + if (jsonOut == null) { + LOG.warn("Unable to save Observations session JSON for sessionId={}", sessionId); + } + } catch (SQLException ex) { + LOG.error("Error saving observations", ex); + JOptionPane.showMessageDialog(this, "Database error saving observations: " + ex.getMessage(), "Database error", JOptionPane.ERROR_MESSAGE); + } + } +} diff --git a/src/main/java/com/studentgui/apppages/ScreenReader.java b/src/main/java/com/studentgui/apppages/ScreenReader.java index 2efddb0..ed8daa0 100644 --- a/src/main/java/com/studentgui/apppages/ScreenReader.java +++ b/src/main/java/com/studentgui/apppages/ScreenReader.java @@ -1,366 +1,429 @@ -package com.studentgui.apppages; - -import java.awt.BorderLayout; -import java.awt.Font; -import java.awt.GridBagConstraints; -import java.awt.GridBagLayout; -import java.awt.Insets; -import java.awt.event.ActionEvent; -import java.awt.event.KeyEvent; -import java.sql.SQLException; -import java.time.LocalDate; -import java.util.List; - -import javax.swing.JButton; -import javax.swing.JLabel; -import javax.swing.JOptionPane; -import javax.swing.JPanel; -import javax.swing.JScrollPane; -import javax.swing.SwingUtilities; - -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; - -/** - * ScreenReader skills progression page. - * - * Displays a form of numeric fields representing screen reader skill codes - * and provides persistence of those values to the canonical database. A - * supplied {@link com.studentgui.apppages.JLineGraph} is used to render - * recent results below the form. - */ -public class ScreenReader extends JPanel implements com.studentgui.app.DateChangeListener, com.studentgui.app.StudentChangeListener { - private static final Logger LOG = LoggerFactory.getLogger(ScreenReader.class); - /** Array of input fields corresponding to ScreenReader assessment parts. */ - private final com.studentgui.uicomp.PhaseScoreField[] skillFields; - /** Canonical parts (code + label) for ScreenReader. */ - private final String[][] parts; - - /** Shared graph component used to visualize recent ScreenReader sessions. */ - private final JLineGraph lineGraph; - - /** Selected student's display name used for saves and plots (may be null). */ - private String studentNameParam; - /** Title label shown at the top of the page. */ - private JLabel titleLabel; - /** Base title used for the Screen Reader page header; date is appended when shown. */ - private final String baseTitle = "Screen Reader Skills Progression"; - - /** Session date associated with entries made on this page. */ - private LocalDate dateParam; - - /** - * Construct a ScreenReader page bound to a student and date. - * The provided JLineGraph is used to render recent assessment results. - * - * @param studentName the student display name (may be null to indicate no selection) - * @param date the date associated with the session - * @param lineGraph chart component used to display recent results - */ - public ScreenReader(String studentName, LocalDate date, JLineGraph lineGraph) { - this.studentNameParam = (studentName == null || studentName.trim().isEmpty()) ? com.studentgui.apphelpers.Helpers.defaultStudent() : studentName; - this.dateParam = date; - this.lineGraph = lineGraph; - setLayout(new BorderLayout()); - - this.parts = new String[][]{ - {"P1_1","1.1 Basic Navigation"},{"P1_2","1.2 Read Labels"},{"P1_3","1.3 Interact Controls"},{"P1_4","1.4 Form Entry"},{"P1_5","1.5 Table Navigation"},{"P1_6","1.6 Headings"}, - {"P2_1","2.1 Links"},{"P2_2","2.2 Lists"},{"P2_3","2.3 Images"},{"P2_4","2.4 Annotations"}, - {"P3_1","3.1 Document Structure"},{"P3_2","3.2 Styles"},{"P3_3","3.3 Tables"},{"P3_4","3.4 Charts"},{"P3_5","3.5 Advanced Shortcuts"},{"P3_6","3.6 Scripting"},{"P3_7","3.7 Third Party Apps"},{"P3_8","3.8 Multimedia"},{"P3_9","3.9 Braille Display Use"},{"P3_10","3.10 Braille Tables"},{"P3_11","3.11Customization"}, - {"P4_1","4.1 Performance"},{"P4_2","4.2 Error Recovery"},{"P4_3","4.3 Integration"},{"P4_4","4.4 Accessibility APIs"},{"P4_5","4.5 Settings"},{"P4_6","4.6 Profiles"},{"P4_7","4.7 Support"} - }; - - JPanel dataEntryPanel = new JPanel(new GridBagLayout()); - JPanel view = new JPanel(new BorderLayout()); - view.add(dataEntryPanel, BorderLayout.NORTH); - view.setBorder(javax.swing.BorderFactory.createEmptyBorder(20,20,20,20)); - JScrollPane scroll = new JScrollPane(view); - - GridBagConstraints gbc = new GridBagConstraints(); - // tighter insets to keep rows within 1-2 lines vertical spacing - gbc.insets = new Insets(2,2,2,2); - gbc.fill = GridBagConstraints.HORIZONTAL; - gbc.anchor = GridBagConstraints.NORTHWEST; // left-align content - gbc.weightx = 1.0; // allow fields to take available width - - this.titleLabel = new JLabel(baseTitle); - this.titleLabel.getAccessibleContext().setAccessibleName("Screen Reader Skills Progression Title"); - // explicit title font for LAF-independence - this.titleLabel.setFont(new java.awt.Font(java.awt.Font.SANS_SERIF, Font.BOLD, 16)); - gbc.gridx = 0; gbc.gridy = 0; gbc.gridwidth = GridBagConstraints.REMAINDER; - dataEntryPanel.add(this.titleLabel, gbc); - - // compute label width using the PhaseScoreField label font (12pt) so wrapping is stable across themes - java.awt.Font labelFont = new java.awt.Font(java.awt.Font.SANS_SERIF, java.awt.Font.PLAIN, 12); - String[] labels = java.util.Arrays.stream(parts).map(x->x[1]).toArray(String[]::new); - int maxPx = com.studentgui.uicomp.PhaseScoreField.computeMaxLabelPixelWidth(labelFont, labels); - // clamp wider so most labels stay on 1-2 lines (200..360 px) - com.studentgui.uicomp.PhaseScoreField.setGlobalLabelWidth(Math.min(360, Math.max(200, maxPx + 50))); - skillFields = new com.studentgui.uicomp.PhaseScoreField[this.parts.length]; - for (int i = 0; i < this.parts.length; i++) { - gbc.gridy = i + 1; - gbc.gridwidth = 2; - gbc.gridx = 0; - com.studentgui.uicomp.PhaseScoreField f = new com.studentgui.uicomp.PhaseScoreField(this.parts[i][1], 0); - f.setName("screenreader_" + this.parts[i][0]); - f.getAccessibleContext().setAccessibleName(this.parts[i][1]); - f.setToolTipText("Enter a numeric score for " + this.parts[i][1]); - skillFields[i] = f; - dataEntryPanel.add(f, gbc); - } - - gbc.gridy = this.parts.length + 2; - gbc.weighty = 0.0; - gbc.gridx = 0; - gbc.gridwidth = 1; - gbc.anchor = GridBagConstraints.WEST; - JButton submit = new JButton("Submit Data"); - submit.setPreferredSize(new java.awt.Dimension(0, 32)); - submit.addActionListener((ActionEvent e) -> { submitData(); refreshGraph(); }); - submit.setMnemonic(KeyEvent.VK_S); - submit.setToolTipText("Save ScreenReader scores for the selected student (Alt+S)"); - submit.getAccessibleContext().setAccessibleName("Submit ScreenReader Data"); - dataEntryPanel.add(submit, gbc); - - gbc.gridx = 1; - JButton openLatest = new JButton("Open Latest Plot"); - openLatest.setPreferredSize(new java.awt.Dimension(0, 32)); - openLatest.addActionListener((ActionEvent e) -> openLatestPlot()); - openLatest.setToolTipText("Open the most recently saved ScreenReader plot for this student"); - openLatest.getAccessibleContext().setAccessibleName("Open Latest ScreenReader Plot"); - dataEntryPanel.add(openLatest, gbc); - - // consume remaining columns so layout stays compact and buttons are not clipped - gbc.gridx = 2; gbc.gridwidth = GridBagConstraints.REMAINDER; gbc.anchor = GridBagConstraints.WEST; - dataEntryPanel.add(new JPanel(), gbc); - - scroll.getAccessibleContext().setAccessibleName("ScreenReader data entry scroll pane"); - add(scroll, BorderLayout.CENTER); - - SwingUtilities.invokeLater(() -> { view.setPreferredSize(view.getPreferredSize()); scroll.getViewport().setViewPosition(new java.awt.Point(0,0)); updateTitleDate(); revalidate(); }); - // Diagnostic: log spinner positions and actual gap after layout - SwingUtilities.invokeLater(() -> { - for (com.studentgui.uicomp.PhaseScoreField f : skillFields) { - if (f != null) { - LOG.debug("ScreenReader field {} labelWidth={} spinnerX={} gap={}", f.getLabel(), f.getLabelWrapWidth(), f.getSpinnerX(), f.getActualGap()); - } - } - }); - - com.studentgui.apphelpers.Helpers.createFolderHierarchy(); - initDatabase(); - // Do not refresh or save graphs automatically on construction to avoid - // writing files or opening images during application startup. - // refreshGraph(); - } - - /** - * Ensure the ScreenReader progress type and its assessment parts exist. - * This is idempotent and safe to call on page creation. - */ - private void initDatabase() { - try { - int ptId = com.studentgui.apphelpers.Database.getOrCreateProgressType("ScreenReader"); - String[] codes = new String[]{ - "P1_1","P1_2","P1_3","P1_4","P1_5","P1_6", - "P2_1","P2_2","P2_3","P2_4", - "P3_1","P3_2","P3_3","P3_4","P3_5","P3_6","P3_7","P3_8","P3_9","P3_10","P3_11", - "P4_1","P4_2","P4_3","P4_4","P4_5","P4_6","P4_7" - }; - com.studentgui.apphelpers.Database.ensureAssessmentParts(ptId, codes); - } catch (SQLException ex) { - LOG.error("Error initializing ScreenReader parts", ex); - } - } - - /** - * Collect values from the entry fields, validate them, and persist - * them to the database as an assessment session. - */ - private void submitData() { - if (this.studentNameParam == null || this.studentNameParam.trim().isEmpty()) { - JOptionPane.showMessageDialog(this, "Please select a student before submitting ScreenReader data.", "Missing student", JOptionPane.WARNING_MESSAGE); - return; - } - try { - int studentId = com.studentgui.apphelpers.Database.getOrCreateStudent(this.studentNameParam); - int ptId = com.studentgui.apphelpers.Database.getOrCreateProgressType("ScreenReader"); - int sessionId = com.studentgui.apphelpers.Database.createProgressSession(studentId, ptId, this.dateParam); - String[] codes = new String[this.parts.length]; - int[] scores = new int[this.parts.length]; - for (int i = 0; i < this.parts.length; i++) { - codes[i] = this.parts[i][0]; - scores[i] = skillFields[i].getValue(); - } - com.studentgui.apphelpers.Database.insertAssessmentResults(sessionId, ptId, codes, scores); - LOG.info("ScreenReader data submitted for student={}", this.studentNameParam); - com.studentgui.apphelpers.UiNotifier.show("ScreenReader data saved."); - com.studentgui.apphelpers.dto.AssessmentPayload payload = new com.studentgui.apphelpers.dto.AssessmentPayload(sessionId, codes, scores); - java.nio.file.Path jsonOut = com.studentgui.apphelpers.SessionJsonWriter.writeSessionJson(this.studentNameParam, "ScreenReader", payload, sessionId); - if (jsonOut == null) { - LOG.warn("Unable to save ScreenReader session JSON for sessionId={}", sessionId); - } - try { - java.nio.file.Path plotsOut = com.studentgui.apphelpers.Helpers.studentPlotsDir(this.studentNameParam); - java.nio.file.Path reportsOut = com.studentgui.apphelpers.Helpers.studentReportsDir(this.studentNameParam); - java.nio.file.Files.createDirectories(plotsOut); - java.nio.file.Files.createDirectories(reportsOut); - java.time.format.DateTimeFormatter df = java.time.format.DateTimeFormatter.ISO_DATE; - String dateStr = this.dateParam != null ? this.dateParam.format(df) : java.time.LocalDate.now().toString(); - String baseName = "ScreenReader-" + sessionId + "-" + dateStr; - - com.studentgui.apphelpers.Database.ResultsWithDates rwd = com.studentgui.apphelpers.Database.fetchLatestAssessmentResultsWithDates(this.studentNameParam, "ScreenReader", Integer.MAX_VALUE); - java.util.Map groups = null; - String[] labels = new String[this.parts.length]; - for (int i = 0; i < this.parts.length; i++) { - labels[i] = this.parts[i][1]; - } - if (rwd != null && rwd.rows != null && !rwd.rows.isEmpty()) { - lineGraph.updateWithGroupedDataByDate(rwd.dates, rwd.rows, codes, labels); - groups = lineGraph.saveGroupedCharts(plotsOut, baseName, 1000, 240); - java.time.LocalDate headerDate = rwd.dates.get(rwd.dates.size() - 1); - dateStr = headerDate.format(df); - } else { - java.util.List> rowsList = new java.util.ArrayList<>(); - java.util.List latest = new java.util.ArrayList<>(); - for (int v : scores) { - latest.add(v); - } - rowsList.add(latest); - lineGraph.updateWithGroupedData(rowsList, codes); - groups = lineGraph.saveGroupedCharts(plotsOut, baseName, 1000, 240); - } - - if (groups == null) { - groups = new java.util.LinkedHashMap<>(); - } - StringBuilder md = new StringBuilder(); - md.append("# ").append(this.studentNameParam == null ? "Unknown Student" : this.studentNameParam).append(" - ").append(dateStr).append("\n\n"); - for (java.util.Map.Entry e : groups.entrySet()) { - md.append("## ").append(e.getKey()).append("\n\n"); - md.append("![](../plots/").append(e.getValue().getFileName().toString()).append(")\n\n"); - } - java.nio.file.Path mdFile = reportsOut.resolve(baseName + ".md"); - java.nio.file.Files.writeString(mdFile, md.toString(), java.nio.charset.StandardCharsets.UTF_8); - - // HTML using shared palette - try { - String[] palette = JLineGraph.PALETTE_HEX; - java.util.LinkedHashMap> groupsIdx = new java.util.LinkedHashMap<>(); - for (int i = 0; i < codes.length; i++) { - String code = codes[i]; - String grp = code != null && code.contains("_") ? code.split("_")[0] : code; - groupsIdx.computeIfAbsent(grp, k -> new java.util.ArrayList<>()).add(i); - } - StringBuilder html = new StringBuilder(); - html.append(""); - html.append(this.studentNameParam == null ? "Student Report" : this.studentNameParam).append(" - ").append(dateStr).append(""); - html.append(""); - html.append(""); - html.append("

").append(this.studentNameParam == null ? "Unknown Student" : this.studentNameParam).append(" - ").append(dateStr).append("

"); - for (java.util.Map.Entry e2 : groups.entrySet()) { - String grp = e2.getKey(); - String imgName = e2.getValue().getFileName().toString(); - html.append("

").append(grp).append("

"); - html.append("
\"").append(grp).append("\"
"); - java.util.List idxs = groupsIdx.getOrDefault(grp, new java.util.ArrayList<>()); - html.append("
"); - for (int s = 0; s < idxs.size(); s++) { - int idx = idxs.get(s); - String code = codes[idx]; - String human = this.parts[idx][1]; - String seriesName = code + " - " + human; - String color = palette[s % palette.length]; - html.append("
"); - html.append(""); - html.append("
"); - html.append(seriesName); - html.append("
"); - } - html.append("
"); - } - html.append(""); - java.nio.file.Path htmlFile = reportsOut.resolve(baseName + ".html"); - java.nio.file.Files.writeString(htmlFile, html.toString(), java.nio.charset.StandardCharsets.UTF_8); - LOG.info("Wrote ScreenReader HTML session report {}", htmlFile); - } catch (java.io.IOException ioex) { - LOG.warn("Unable to write ScreenReader HTML report: {}", ioex.toString()); - } - - LOG.info("Wrote ScreenReader session report {} with {} group images", mdFile, groups.size()); - } catch (java.io.IOException | SQLException ex) { - LOG.warn("Unable to save ScreenReader per-phase plots or markdown report: {}", ex.toString()); - } - } catch (NumberFormatException ex) { - LOG.warn("Invalid number in skill fields", ex); - } catch (SQLException ex) { - LOG.error("DB error submitting ScreenReader data", ex); - JOptionPane.showMessageDialog(this, "Database error saving ScreenReader data: " + ex.getMessage(), "Database error", JOptionPane.ERROR_MESSAGE); - } - } - - /** - * Refresh the attached JLineGraph with the latest ScreenReader data for - * the configured student. - */ - private void refreshGraph() { - try { - List> allSkillValues = com.studentgui.apphelpers.Database.fetchLatestAssessmentResults(studentNameParam, "ScreenReader", 5); - if (allSkillValues != null && !allSkillValues.isEmpty()) { - String[] codes = new String[this.parts.length]; - for (int i = 0; i < this.parts.length; i++) { - codes[i] = this.parts[i][0]; - } - lineGraph.updateWithGroupedData(allSkillValues, codes); - LOG.info("Graph updated with {} series", allSkillValues.size()); - } else { - LOG.info("No ScreenReader data to plot for {}", studentNameParam); - } - } catch (SQLException ex) { - LOG.error("Error fetching ScreenReader data", ex); - } - - // Do not save chart images during refresh to avoid creating files on app startup. - LOG.debug("Skipping auto-save of ScreenReader chart during refresh for student={}", this.studentNameParam); - } - - @Override - public void dateChanged(LocalDate newDate) { - this.dateParam = newDate; - SwingUtilities.invokeLater(() -> { - refreshGraph(); - updateTitleDate(); - }); - } - - @Override - public void studentChanged(String newStudent) { - this.studentNameParam = newStudent; - SwingUtilities.invokeLater(() -> { - refreshGraph(); - updateTitleDate(); - }); - } - - private void updateTitleDate() { - try { - String dateStr = this.dateParam != null ? this.dateParam.toString() : java.time.LocalDate.now().toString(); - this.titleLabel.setText(baseTitle + " - " + dateStr); - } catch (Exception ex) { - this.titleLabel.setText(baseTitle); - } - } - - private void openLatestPlot() { - java.nio.file.Path p = com.studentgui.apphelpers.Helpers.latestPlotPath(this.studentNameParam, "ScreenReader"); - if (p == null) { - com.studentgui.apphelpers.UiNotifier.show("No ScreenReader plot found for student"); - return; - } - try { java.awt.Desktop.getDesktop().open(p.toFile()); } - catch (java.io.IOException | UnsupportedOperationException | SecurityException ex) { com.studentgui.apphelpers.UiNotifier.show("Unable to open plot: " + p.getFileName().toString()); } - } - -} +package com.studentgui.apppages; + +import java.awt.BorderLayout; +import java.awt.Font; +import java.awt.GridBagConstraints; +import java.awt.GridBagLayout; +import java.awt.Insets; +import java.awt.event.ActionEvent; +import java.awt.event.KeyEvent; +import java.sql.SQLException; +import java.time.LocalDate; +import java.util.List; + +import javax.swing.JButton; +import javax.swing.JLabel; +import javax.swing.JOptionPane; +import javax.swing.JPanel; +import javax.swing.JScrollPane; +import javax.swing.SwingUtilities; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + * Screen reader proficiency assessment page for desktop/laptop environments. + * + *

Evaluates student competency with screen reading software (JAWS, NVDA, Narrator, + * VoiceOver macOS) across 28 standardized skills organized into 4 progressive competency phases:

+ * + *
    + *
  • Phase 1 (P1_1–P1_6): Fundamental Navigation and Interaction + *
      + *
    • Basic keyboard navigation (Tab, arrow keys, application switching)
    • + *
    • Reading and interpreting control labels and text content
    • + *
    • Activating controls (buttons, links, checkboxes) via keyboard
    • + *
    • Form entry (text fields, combo boxes, radio buttons)
    • + *
    • Table navigation (row/column movement, header announcement)
    • + *
    • Heading navigation (H key, heading list, semantic structure)
    • + *
    + *
  • + *
  • Phase 2 (P2_1–P2_4): Web and Document Element Navigation + *
      + *
    • Link navigation and link list usage
    • + *
    • List navigation (ordered, unordered, nested lists)
    • + *
    • Image handling (alt text, long descriptions, graphics navigation)
    • + *
    • Annotation and metadata awareness (ARIA labels, landmarks)
    • + *
    + *
  • + *
  • Phase 3 (P3_1–P3_11): Advanced Document Structures and Customization + *
      + *
    • Document structure navigation (sections, articles, landmarks)
    • + *
    • Style and formatting awareness (bold, italic, font changes)
    • + *
    • Advanced table navigation (complex tables, merged cells, formulas)
    • + *
    • Chart and graph interpretation with screen reader feedback
    • + *
    • Advanced keyboard shortcuts and quick navigation commands
    • + *
    • Scripting usage (JAWS scripts, NVDA add-ons)
    • + *
    • Third-party application integration (Office, Adobe, IDEs)
    • + *
    • Multimedia content handling (audio descriptions, video captions)
    • + *
    • Braille display usage and synchronization
    • + *
    • Braille table switching (Grade 1, Grade 2, computer braille)
    • + *
    • Configuration and customization (speech rate, verbosity, sounds)
    • + *
    + *
  • + *
  • Phase 4 (P4_1–P4_7): Efficiency, Troubleshooting, and Integration + *
      + *
    • Performance optimization (adjusting verbosity, quick navigation mastery)
    • + *
    • Error recovery strategies (finding lost focus, restarting speech)
    • + *
    • Integration across multiple assistive technologies (magnification, braille, OCR)
    • + *
    • Accessibility API awareness (UI Automation, MSAA, IAccessible2)
    • + *
    • Settings management (profiles, application-specific configurations)
    • + *
    • Profile creation and switching for different workflows/applications
    • + *
    • Accessing vendor support resources and community forums
    • + *
    + *
  • + *
+ * + *

Data Persistence and Report Generation:

+ *
    + *
  • Scores captured via {@link com.studentgui.uicomp.PhaseScoreField} components (integer 0–4 typical)
  • + *
  • Persisted to normalized schema via {@link com.studentgui.apphelpers.Database#insertAssessmentResults}
  • + *
  • JSON export: {@code StudentDataFiles//Sessions/ScreenReader/ScreenReader--.json}
  • + *
  • Phase-grouped time-series PNG plots: {@code plots/ScreenReader---P.png} (4 phase groups)
  • + *
  • Markdown report: {@code reports/ScreenReader--.md} with relative image links
  • + *
  • HTML report: {@code reports/ScreenReader--.html} with inline styles and legends
  • + *
+ * + *

The shared {@link JLineGraph} visualizes recent session trends with phase-based grouping. + * Implements {@link com.studentgui.app.DateChangeListener} and {@link com.studentgui.app.StudentChangeListener} + * for dynamic refresh when global selections change.

+ * + * @see com.studentgui.apphelpers.Database + * @see JLineGraph + * @see com.studentgui.uicomp.PhaseScoreField + */ +public class ScreenReader extends JPanel implements com.studentgui.app.DateChangeListener, com.studentgui.app.StudentChangeListener { + private static final Logger LOG = LoggerFactory.getLogger(ScreenReader.class); + /** Array of input fields corresponding to ScreenReader assessment parts. */ + private final com.studentgui.uicomp.PhaseScoreField[] skillFields; + /** Canonical parts (code + label) for ScreenReader. */ + private final String[][] parts; + + /** Shared graph component used to visualize recent ScreenReader sessions. */ + private final JLineGraph lineGraph; + + /** Selected student's display name used for saves and plots (may be null). */ + private String studentNameParam; + /** Title label shown at the top of the page. */ + private JLabel titleLabel; + /** Base title used for the Screen Reader page header; date is appended when shown. */ + private final String baseTitle = "Screen Reader Skills Progression"; + + /** Session date associated with entries made on this page. */ + private LocalDate dateParam; + + /** + * Construct a ScreenReader page bound to a student and date. + * The provided JLineGraph is used to render recent assessment results. + * + * @param studentName the student display name (may be null to indicate no selection) + * @param date the date associated with the session + * @param lineGraph chart component used to display recent results + */ + public ScreenReader(String studentName, LocalDate date, JLineGraph lineGraph) { + this.studentNameParam = (studentName == null || studentName.trim().isEmpty()) ? com.studentgui.apphelpers.Helpers.defaultStudent() : studentName; + this.dateParam = date; + this.lineGraph = lineGraph; + setLayout(new BorderLayout()); + + this.parts = new String[][]{ + {"P1_1","1.1 Basic Navigation"},{"P1_2","1.2 Read Labels"},{"P1_3","1.3 Interact Controls"},{"P1_4","1.4 Form Entry"},{"P1_5","1.5 Table Navigation"},{"P1_6","1.6 Headings"}, + {"P2_1","2.1 Links"},{"P2_2","2.2 Lists"},{"P2_3","2.3 Images"},{"P2_4","2.4 Annotations"}, + {"P3_1","3.1 Document Structure"},{"P3_2","3.2 Styles"},{"P3_3","3.3 Tables"},{"P3_4","3.4 Charts"},{"P3_5","3.5 Advanced Shortcuts"},{"P3_6","3.6 Scripting"},{"P3_7","3.7 Third Party Apps"},{"P3_8","3.8 Multimedia"},{"P3_9","3.9 Braille Display Use"},{"P3_10","3.10 Braille Tables"},{"P3_11","3.11Customization"}, + {"P4_1","4.1 Performance"},{"P4_2","4.2 Error Recovery"},{"P4_3","4.3 Integration"},{"P4_4","4.4 Accessibility APIs"},{"P4_5","4.5 Settings"},{"P4_6","4.6 Profiles"},{"P4_7","4.7 Support"} + }; + + JPanel dataEntryPanel = new JPanel(new GridBagLayout()); + JPanel view = new JPanel(new BorderLayout()); + view.add(dataEntryPanel, BorderLayout.NORTH); + view.setBorder(javax.swing.BorderFactory.createEmptyBorder(20,20,20,20)); + JScrollPane scroll = new JScrollPane(view); + + GridBagConstraints gbc = new GridBagConstraints(); + // tighter insets to keep rows within 1-2 lines vertical spacing + gbc.insets = new Insets(2,2,2,2); + gbc.fill = GridBagConstraints.HORIZONTAL; + gbc.anchor = GridBagConstraints.NORTHWEST; // left-align content + gbc.weightx = 1.0; // allow fields to take available width + + this.titleLabel = new JLabel(baseTitle); + this.titleLabel.getAccessibleContext().setAccessibleName("Screen Reader Skills Progression Title"); + // explicit title font for LAF-independence + this.titleLabel.setFont(new java.awt.Font(java.awt.Font.SANS_SERIF, Font.BOLD, 16)); + gbc.gridx = 0; gbc.gridy = 0; gbc.gridwidth = GridBagConstraints.REMAINDER; + dataEntryPanel.add(this.titleLabel, gbc); + + // compute label width using the PhaseScoreField label font (12pt) so wrapping is stable across themes + java.awt.Font labelFont = new java.awt.Font(java.awt.Font.SANS_SERIF, java.awt.Font.PLAIN, 12); + String[] labels = java.util.Arrays.stream(parts).map(x->x[1]).toArray(String[]::new); + int maxPx = com.studentgui.uicomp.PhaseScoreField.computeMaxLabelPixelWidth(labelFont, labels); + // clamp wider so most labels stay on 1-2 lines (200..360 px) + com.studentgui.uicomp.PhaseScoreField.setGlobalLabelWidth(Math.min(360, Math.max(200, maxPx + 50))); + skillFields = new com.studentgui.uicomp.PhaseScoreField[this.parts.length]; + for (int i = 0; i < this.parts.length; i++) { + gbc.gridy = i + 1; + gbc.gridwidth = 2; + gbc.gridx = 0; + com.studentgui.uicomp.PhaseScoreField f = new com.studentgui.uicomp.PhaseScoreField(this.parts[i][1], 0); + f.setName("screenreader_" + this.parts[i][0]); + f.getAccessibleContext().setAccessibleName(this.parts[i][1]); + f.setToolTipText("Enter a numeric score for " + this.parts[i][1]); + skillFields[i] = f; + dataEntryPanel.add(f, gbc); + } + + gbc.gridy = this.parts.length + 2; + gbc.weighty = 0.0; + gbc.gridx = 0; + gbc.gridwidth = 1; + gbc.anchor = GridBagConstraints.WEST; + JButton submit = new JButton("Submit Data"); + submit.setPreferredSize(new java.awt.Dimension(0, 32)); + submit.addActionListener((ActionEvent e) -> { submitData(); refreshGraph(); }); + submit.setMnemonic(KeyEvent.VK_S); + submit.setToolTipText("Save ScreenReader scores for the selected student (Alt+S)"); + submit.getAccessibleContext().setAccessibleName("Submit ScreenReader Data"); + dataEntryPanel.add(submit, gbc); + + gbc.gridx = 1; + JButton openLatest = new JButton("Open Latest Plot"); + openLatest.setPreferredSize(new java.awt.Dimension(0, 32)); + openLatest.addActionListener((ActionEvent e) -> openLatestPlot()); + openLatest.setToolTipText("Open the most recently saved ScreenReader plot for this student"); + openLatest.getAccessibleContext().setAccessibleName("Open Latest ScreenReader Plot"); + dataEntryPanel.add(openLatest, gbc); + + // consume remaining columns so layout stays compact and buttons are not clipped + gbc.gridx = 2; gbc.gridwidth = GridBagConstraints.REMAINDER; gbc.anchor = GridBagConstraints.WEST; + dataEntryPanel.add(new JPanel(), gbc); + + scroll.getAccessibleContext().setAccessibleName("ScreenReader data entry scroll pane"); + add(scroll, BorderLayout.CENTER); + + SwingUtilities.invokeLater(() -> { view.setPreferredSize(view.getPreferredSize()); scroll.getViewport().setViewPosition(new java.awt.Point(0,0)); updateTitleDate(); revalidate(); }); + // Diagnostic: log spinner positions and actual gap after layout + SwingUtilities.invokeLater(() -> { + for (com.studentgui.uicomp.PhaseScoreField f : skillFields) { + if (f != null) { + LOG.debug("ScreenReader field {} labelWidth={} spinnerX={} gap={}", f.getLabel(), f.getLabelWrapWidth(), f.getSpinnerX(), f.getActualGap()); + } + } + }); + + com.studentgui.apphelpers.Helpers.createFolderHierarchy(); + initDatabase(); + // Do not refresh or save graphs automatically on construction to avoid + // writing files or opening images during application startup. + // refreshGraph(); + } + + /** + * Ensure the ScreenReader progress type and its assessment parts exist. + * This is idempotent and safe to call on page creation. + */ + private void initDatabase() { + try { + int ptId = com.studentgui.apphelpers.Database.getOrCreateProgressType("ScreenReader"); + String[] codes = new String[]{ + "P1_1","P1_2","P1_3","P1_4","P1_5","P1_6", + "P2_1","P2_2","P2_3","P2_4", + "P3_1","P3_2","P3_3","P3_4","P3_5","P3_6","P3_7","P3_8","P3_9","P3_10","P3_11", + "P4_1","P4_2","P4_3","P4_4","P4_5","P4_6","P4_7" + }; + com.studentgui.apphelpers.Database.ensureAssessmentParts(ptId, codes); + } catch (SQLException ex) { + LOG.error("Error initializing ScreenReader parts", ex); + } + } + + /** + * Collect values from the entry fields, validate them, and persist + * them to the database as an assessment session. + */ + private void submitData() { + if (this.studentNameParam == null || this.studentNameParam.trim().isEmpty()) { + JOptionPane.showMessageDialog(this, "Please select a student before submitting ScreenReader data.", "Missing student", JOptionPane.WARNING_MESSAGE); + return; + } + try { + int studentId = com.studentgui.apphelpers.Database.getOrCreateStudent(this.studentNameParam); + int ptId = com.studentgui.apphelpers.Database.getOrCreateProgressType("ScreenReader"); + int sessionId = com.studentgui.apphelpers.Database.createProgressSession(studentId, ptId, this.dateParam); + String[] codes = new String[this.parts.length]; + int[] scores = new int[this.parts.length]; + for (int i = 0; i < this.parts.length; i++) { + codes[i] = this.parts[i][0]; + scores[i] = skillFields[i].getValue(); + } + com.studentgui.apphelpers.Database.insertAssessmentResults(sessionId, ptId, codes, scores); + LOG.info("ScreenReader data submitted for student={}", this.studentNameParam); + com.studentgui.apphelpers.UiNotifier.show("ScreenReader data saved."); + com.studentgui.apphelpers.dto.AssessmentPayload payload = new com.studentgui.apphelpers.dto.AssessmentPayload(sessionId, codes, scores); + java.nio.file.Path jsonOut = com.studentgui.apphelpers.SessionJsonWriter.writeSessionJson(this.studentNameParam, "ScreenReader", payload, sessionId); + if (jsonOut == null) { + LOG.warn("Unable to save ScreenReader session JSON for sessionId={}", sessionId); + } + try { + java.nio.file.Path plotsOut = com.studentgui.apphelpers.Helpers.studentPlotsDir(this.studentNameParam); + java.nio.file.Path reportsOut = com.studentgui.apphelpers.Helpers.studentReportsDir(this.studentNameParam); + java.nio.file.Files.createDirectories(plotsOut); + java.nio.file.Files.createDirectories(reportsOut); + java.time.format.DateTimeFormatter df = java.time.format.DateTimeFormatter.ISO_DATE; + String dateStr = this.dateParam != null ? this.dateParam.format(df) : java.time.LocalDate.now().toString(); + String baseName = "ScreenReader-" + sessionId + "-" + dateStr; + + com.studentgui.apphelpers.Database.ResultsWithDates rwd = com.studentgui.apphelpers.Database.fetchLatestAssessmentResultsWithDates(this.studentNameParam, "ScreenReader", Integer.MAX_VALUE); + java.util.Map groups = null; + String[] labels = new String[this.parts.length]; + for (int i = 0; i < this.parts.length; i++) { + labels[i] = this.parts[i][1]; + } + if (rwd != null && rwd.rows != null && !rwd.rows.isEmpty()) { + lineGraph.updateWithGroupedDataByDate(rwd.dates, rwd.rows, codes, labels); + groups = lineGraph.saveGroupedCharts(plotsOut, baseName, 1000, 240); + java.time.LocalDate headerDate = rwd.dates.get(rwd.dates.size() - 1); + dateStr = headerDate.format(df); + } else { + java.util.List> rowsList = new java.util.ArrayList<>(); + java.util.List latest = new java.util.ArrayList<>(); + for (int v : scores) { + latest.add(v); + } + rowsList.add(latest); + lineGraph.updateWithGroupedData(rowsList, codes); + groups = lineGraph.saveGroupedCharts(plotsOut, baseName, 1000, 240); + } + + if (groups == null) { + groups = new java.util.LinkedHashMap<>(); + } + StringBuilder md = new StringBuilder(); + md.append("# ").append(this.studentNameParam == null ? "Unknown Student" : this.studentNameParam).append(" - ").append(dateStr).append("\n\n"); + for (java.util.Map.Entry e : groups.entrySet()) { + md.append("## ").append(e.getKey()).append("\n\n"); + md.append("![](../plots/").append(e.getValue().getFileName().toString()).append(")\n\n"); + } + java.nio.file.Path mdFile = reportsOut.resolve(baseName + ".md"); + java.nio.file.Files.writeString(mdFile, md.toString(), java.nio.charset.StandardCharsets.UTF_8); + + // HTML using shared palette + try { + String[] palette = JLineGraph.PALETTE_HEX; + java.util.LinkedHashMap> groupsIdx = new java.util.LinkedHashMap<>(); + for (int i = 0; i < codes.length; i++) { + String code = codes[i]; + String grp = code != null && code.contains("_") ? code.split("_")[0] : code; + groupsIdx.computeIfAbsent(grp, k -> new java.util.ArrayList<>()).add(i); + } + StringBuilder html = new StringBuilder(); + html.append(""); + html.append(this.studentNameParam == null ? "Student Report" : this.studentNameParam).append(" - ").append(dateStr).append(""); + html.append(""); + html.append(""); + html.append("

").append(this.studentNameParam == null ? "Unknown Student" : this.studentNameParam).append(" - ").append(dateStr).append("

"); + for (java.util.Map.Entry e2 : groups.entrySet()) { + String grp = e2.getKey(); + String imgName = e2.getValue().getFileName().toString(); + html.append("

").append(grp).append("

"); + html.append("
\"").append(grp).append("\"
"); + java.util.List idxs = groupsIdx.getOrDefault(grp, new java.util.ArrayList<>()); + html.append("
"); + for (int s = 0; s < idxs.size(); s++) { + int idx = idxs.get(s); + String code = codes[idx]; + String human = this.parts[idx][1]; + String seriesName = code + " - " + human; + String color = palette[s % palette.length]; + html.append("
"); + html.append(""); + html.append("
"); + html.append(seriesName); + html.append("
"); + } + html.append("
"); + } + html.append(""); + java.nio.file.Path htmlFile = reportsOut.resolve(baseName + ".html"); + java.nio.file.Files.writeString(htmlFile, html.toString(), java.nio.charset.StandardCharsets.UTF_8); + LOG.info("Wrote ScreenReader HTML session report {}", htmlFile); + } catch (java.io.IOException ioex) { + LOG.warn("Unable to write ScreenReader HTML report: {}", ioex.toString()); + } + + LOG.info("Wrote ScreenReader session report {} with {} group images", mdFile, groups.size()); + } catch (java.io.IOException | SQLException ex) { + LOG.warn("Unable to save ScreenReader per-phase plots or markdown report: {}", ex.toString()); + } + } catch (NumberFormatException ex) { + LOG.warn("Invalid number in skill fields", ex); + } catch (SQLException ex) { + LOG.error("DB error submitting ScreenReader data", ex); + JOptionPane.showMessageDialog(this, "Database error saving ScreenReader data: " + ex.getMessage(), "Database error", JOptionPane.ERROR_MESSAGE); + } + } + + /** + * Refresh the attached JLineGraph with the latest ScreenReader data for + * the configured student. + */ + private void refreshGraph() { + try { + List> allSkillValues = com.studentgui.apphelpers.Database.fetchLatestAssessmentResults(studentNameParam, "ScreenReader", 5); + if (allSkillValues != null && !allSkillValues.isEmpty()) { + String[] codes = new String[this.parts.length]; + for (int i = 0; i < this.parts.length; i++) { + codes[i] = this.parts[i][0]; + } + lineGraph.updateWithGroupedData(allSkillValues, codes); + LOG.info("Graph updated with {} series", allSkillValues.size()); + } else { + LOG.info("No ScreenReader data to plot for {}", studentNameParam); + } + } catch (SQLException ex) { + LOG.error("Error fetching ScreenReader data", ex); + } + + // Do not save chart images during refresh to avoid creating files on app startup. + LOG.debug("Skipping auto-save of ScreenReader chart during refresh for student={}", this.studentNameParam); + } + + @Override + public void dateChanged(LocalDate newDate) { + this.dateParam = newDate; + SwingUtilities.invokeLater(() -> { + refreshGraph(); + updateTitleDate(); + }); + } + + @Override + public void studentChanged(String newStudent) { + this.studentNameParam = newStudent; + SwingUtilities.invokeLater(() -> { + refreshGraph(); + updateTitleDate(); + }); + } + + private void updateTitleDate() { + try { + String dateStr = this.dateParam != null ? this.dateParam.toString() : java.time.LocalDate.now().toString(); + this.titleLabel.setText(baseTitle + " - " + dateStr); + } catch (Exception ex) { + this.titleLabel.setText(baseTitle); + } + } + + private void openLatestPlot() { + java.nio.file.Path p = com.studentgui.apphelpers.Helpers.latestPlotPath(this.studentNameParam, "ScreenReader"); + if (p == null) { + com.studentgui.apphelpers.UiNotifier.show("No ScreenReader plot found for student"); + return; + } + try { java.awt.Desktop.getDesktop().open(p.toFile()); } + catch (java.io.IOException | UnsupportedOperationException | SecurityException ex) { com.studentgui.apphelpers.UiNotifier.show("Unable to open plot: " + p.getFileName().toString()); } + } + +} diff --git a/src/main/java/com/studentgui/apppages/SessionNotes.java b/src/main/java/com/studentgui/apppages/SessionNotes.java index 7d9c60a..8c77885 100644 --- a/src/main/java/com/studentgui/apppages/SessionNotes.java +++ b/src/main/java/com/studentgui/apppages/SessionNotes.java @@ -1,110 +1,138 @@ -package com.studentgui.apppages; - -import java.awt.BorderLayout; -import java.awt.Font; -import java.awt.GridBagConstraints; -import java.awt.GridBagLayout; -import java.awt.Insets; -import java.awt.event.ActionEvent; -import java.awt.event.KeyEvent; -import java.sql.SQLException; -import java.time.LocalDate; - -import javax.swing.JButton; -import javax.swing.JLabel; -import javax.swing.JPanel; -import javax.swing.JScrollPane; -import javax.swing.JTextArea; -import javax.swing.SwingUtilities; - -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; - -/** - * Session notes editor page. - */ -public class SessionNotes extends JPanel { - private static final Logger LOG = LoggerFactory.getLogger(SessionNotes.class); - /** Text area containing session notes entered by the user. */ - private final JTextArea notesArea; - - /** Selected student's display name used when saving session notes (may be null). */ - private final String studentNameParam; - - /** Date associated with these session notes. */ - private final LocalDate dateParam; - - /** - * Create a SessionNotes page for the provided student and date. - * The supplied JLineGraph is displayed below the notes editor. - * - * @param studentName student display name (may be null when no student selected) - * @param date the date this session pertains to - * @param graph the chart component shown beneath the notes - */ - public SessionNotes(String studentName, LocalDate date, JLineGraph graph) { - this.studentNameParam = (studentName == null || studentName.trim().isEmpty()) ? com.studentgui.apphelpers.Helpers.defaultStudent() : studentName; - this.dateParam = date; - setLayout(new BorderLayout()); - - JPanel p = new JPanel(new GridBagLayout()); - JPanel view = new JPanel(new BorderLayout()); - view.add(p, BorderLayout.NORTH); - view.setBorder(javax.swing.BorderFactory.createEmptyBorder(20,20,20,20)); - JScrollPane scroll = new JScrollPane(view); - scroll.getAccessibleContext().setAccessibleName("Session Notes data entry scroll pane"); - GridBagConstraints gbc = new GridBagConstraints(); gbc.insets=new Insets(2,2,2,2); gbc.fill = GridBagConstraints.BOTH; gbc.anchor = GridBagConstraints.NORTHWEST; - JLabel title = new JLabel("Session Notes", JLabel.LEFT); - title.setFont(new Font(Font.SANS_SERIF, Font.BOLD, 16)); - title.getAccessibleContext().setAccessibleName("Session Notes Title"); - gbc.gridx=0; gbc.gridy=0; gbc.gridwidth=1; p.add(title, gbc); - - int globalLabel = com.studentgui.uicomp.PhaseScoreField.getGlobalLabelWidth(); - gbc.gridy=1; gbc.gridx=0; JLabel notesLabel = new JLabel("Notes:"); notesLabel.setPreferredSize(new java.awt.Dimension(globalLabel, notesLabel.getPreferredSize().height)); p.add(notesLabel, gbc); - gbc.gridy=2; gbc.gridx=0; notesArea = new JTextArea(8,40); notesArea.setLineWrap(true); notesArea.setWrapStyleWord(true); notesArea.setToolTipText("Enter session notes for the student"); notesArea.getAccessibleContext().setAccessibleName("Session notes"); p.add(notesArea, gbc); - notesLabel.setLabelFor(notesArea); - - gbc.gridy=3; JButton submit = new JButton("Save Session Notes"); - submit.addActionListener((ActionEvent e)-> saveNotes()); - submit.setMnemonic(KeyEvent.VK_S); - submit.setToolTipText("Save session notes (Alt+S)"); - submit.getAccessibleContext().setAccessibleName("Save Session Notes"); - p.add(submit, gbc); - - add(scroll, BorderLayout.CENTER); - - SwingUtilities.invokeLater(()->{ p.setPreferredSize(p.getPreferredSize()); revalidate(); }); - - com.studentgui.apphelpers.Helpers.createFolderHierarchy(); - } - - /** - * Persist the contents of the session notes into the database. Ensures - * required student and progress session records exist and writes the notes - * to the ProgressSession.notes column. - */ - private void saveNotes() { - if (this.studentNameParam == null || this.studentNameParam.trim().isEmpty()) { - javax.swing.JOptionPane.showMessageDialog(this, "Please select a student before saving session notes.", "Missing student", javax.swing.JOptionPane.WARNING_MESSAGE); - return; - } - - try { - int studentId = com.studentgui.apphelpers.Database.getOrCreateStudent(this.studentNameParam); - int ptId = com.studentgui.apphelpers.Database.getOrCreateProgressType("SessionNotes"); - int sessionId = com.studentgui.apphelpers.Database.createProgressSession(studentId, ptId, this.dateParam); - String notes = notesArea.getText(); - com.studentgui.apphelpers.Database.saveSessionNotes(sessionId, notes); - LOG.info("Saved session notes for {}", studentNameParam); - com.studentgui.apphelpers.UiNotifier.show("Session notes saved."); - com.studentgui.apphelpers.dto.NotesPayload payload = new com.studentgui.apphelpers.dto.NotesPayload(sessionId, notes); - java.nio.file.Path jsonOut = com.studentgui.apphelpers.SessionJsonWriter.writeSessionJson(this.studentNameParam, "SessionNotes", payload, sessionId); - if (jsonOut == null) { - LOG.warn("Unable to save SessionNotes session JSON for sessionId={}", sessionId); - } - } catch (SQLException ex) { - LOG.error("Error saving session notes", ex); - javax.swing.JOptionPane.showMessageDialog(this, "Database error saving session notes: " + ex.getMessage(), "Database error", javax.swing.JOptionPane.ERROR_MESSAGE); - } - } -} +package com.studentgui.apppages; + +import java.awt.BorderLayout; +import java.awt.Font; +import java.awt.GridBagConstraints; +import java.awt.GridBagLayout; +import java.awt.Insets; +import java.awt.event.ActionEvent; +import java.awt.event.KeyEvent; +import java.sql.SQLException; +import java.time.LocalDate; + +import javax.swing.JButton; +import javax.swing.JLabel; +import javax.swing.JPanel; +import javax.swing.JScrollPane; +import javax.swing.JTextArea; +import javax.swing.SwingUtilities; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + * Freeform session notes editor for general observations and reflections. + * + *

Provides a simple multi-line text area for educators to record unstructured notes + * about a student session. This complements the structured assessment pages (Braille, Abacus, etc.) + * by allowing qualitative observations, anecdotal records, and contextual details that don't + * fit into numeric scoring fields.

+ * + *

Typical Use Cases:

+ *
    + *
  • Recording behavioral observations (e.g., "Student showed increased frustration with Nemeth fractions today")
  • + *
  • Documenting environmental factors affecting performance (e.g., "Noisy classroom due to construction")
  • + *
  • Noting equipment issues or accommodations used (e.g., "Switched to Braille Sense due to BrailleNote malfunction")
  • + *
  • General reflections or instructional notes for future reference
  • + *
+ * + *

Data Storage:

+ *
    + *
  • Notes persisted via {@link com.studentgui.apphelpers.Database#saveSessionNotes} to {@code ProgressSession.notes} column
  • + *
  • Associated with a SessionNotes progress type and session ID for consistent querying
  • + *
  • JSON export: {@code StudentDataFiles//Sessions/SessionNotes/SessionNotes--.json}
  • + *
  • No plots or reports generated (text-only data)
  • + *
+ * + *

The shared {@link JLineGraph} component is present for UI layout consistency but remains + * empty (session notes are not quantitative data). This page does not implement listener interfaces + * as it operates on static student/date parameters provided at construction time.

+ * + * @see com.studentgui.apphelpers.Database#saveSessionNotes + * @see com.studentgui.apphelpers.dto.NotesPayload + */ +public class SessionNotes extends JPanel { + private static final Logger LOG = LoggerFactory.getLogger(SessionNotes.class); + /** Text area containing session notes entered by the user. */ + private final JTextArea notesArea; + + /** Selected student's display name used when saving session notes (may be null). */ + private final String studentNameParam; + + /** Date associated with these session notes. */ + private final LocalDate dateParam; + + /** + * Create a SessionNotes page for the provided student and date. + * The supplied JLineGraph is displayed below the notes editor. + * + * @param studentName student display name (may be null when no student selected) + * @param date the date this session pertains to + * @param graph the chart component shown beneath the notes + */ + public SessionNotes(String studentName, LocalDate date, JLineGraph graph) { + this.studentNameParam = (studentName == null || studentName.trim().isEmpty()) ? com.studentgui.apphelpers.Helpers.defaultStudent() : studentName; + this.dateParam = date; + setLayout(new BorderLayout()); + + JPanel p = new JPanel(new GridBagLayout()); + JPanel view = new JPanel(new BorderLayout()); + view.add(p, BorderLayout.NORTH); + view.setBorder(javax.swing.BorderFactory.createEmptyBorder(20,20,20,20)); + JScrollPane scroll = new JScrollPane(view); + scroll.getAccessibleContext().setAccessibleName("Session Notes data entry scroll pane"); + GridBagConstraints gbc = new GridBagConstraints(); gbc.insets=new Insets(2,2,2,2); gbc.fill = GridBagConstraints.BOTH; gbc.anchor = GridBagConstraints.NORTHWEST; + JLabel title = new JLabel("Session Notes", JLabel.LEFT); + title.setFont(new Font(Font.SANS_SERIF, Font.BOLD, 16)); + title.getAccessibleContext().setAccessibleName("Session Notes Title"); + gbc.gridx=0; gbc.gridy=0; gbc.gridwidth=1; p.add(title, gbc); + + int globalLabel = com.studentgui.uicomp.PhaseScoreField.getGlobalLabelWidth(); + gbc.gridy=1; gbc.gridx=0; JLabel notesLabel = new JLabel("Notes:"); notesLabel.setPreferredSize(new java.awt.Dimension(globalLabel, notesLabel.getPreferredSize().height)); p.add(notesLabel, gbc); + gbc.gridy=2; gbc.gridx=0; notesArea = new JTextArea(8,40); notesArea.setLineWrap(true); notesArea.setWrapStyleWord(true); notesArea.setToolTipText("Enter session notes for the student"); notesArea.getAccessibleContext().setAccessibleName("Session notes"); p.add(notesArea, gbc); + notesLabel.setLabelFor(notesArea); + + gbc.gridy=3; JButton submit = new JButton("Save Session Notes"); + submit.addActionListener((ActionEvent e)-> saveNotes()); + submit.setMnemonic(KeyEvent.VK_S); + submit.setToolTipText("Save session notes (Alt+S)"); + submit.getAccessibleContext().setAccessibleName("Save Session Notes"); + p.add(submit, gbc); + + add(scroll, BorderLayout.CENTER); + + SwingUtilities.invokeLater(()->{ p.setPreferredSize(p.getPreferredSize()); revalidate(); }); + + com.studentgui.apphelpers.Helpers.createFolderHierarchy(); + } + + /** + * Persist the contents of the session notes into the database. Ensures + * required student and progress session records exist and writes the notes + * to the ProgressSession.notes column. + */ + private void saveNotes() { + if (this.studentNameParam == null || this.studentNameParam.trim().isEmpty()) { + javax.swing.JOptionPane.showMessageDialog(this, "Please select a student before saving session notes.", "Missing student", javax.swing.JOptionPane.WARNING_MESSAGE); + return; + } + + try { + int studentId = com.studentgui.apphelpers.Database.getOrCreateStudent(this.studentNameParam); + int ptId = com.studentgui.apphelpers.Database.getOrCreateProgressType("SessionNotes"); + int sessionId = com.studentgui.apphelpers.Database.createProgressSession(studentId, ptId, this.dateParam); + String notes = notesArea.getText(); + com.studentgui.apphelpers.Database.saveSessionNotes(sessionId, notes); + LOG.info("Saved session notes for {}", studentNameParam); + com.studentgui.apphelpers.UiNotifier.show("Session notes saved."); + com.studentgui.apphelpers.dto.NotesPayload payload = new com.studentgui.apphelpers.dto.NotesPayload(sessionId, notes); + java.nio.file.Path jsonOut = com.studentgui.apphelpers.SessionJsonWriter.writeSessionJson(this.studentNameParam, "SessionNotes", payload, sessionId); + if (jsonOut == null) { + LOG.warn("Unable to save SessionNotes session JSON for sessionId={}", sessionId); + } + } catch (SQLException ex) { + LOG.error("Error saving session notes", ex); + javax.swing.JOptionPane.showMessageDialog(this, "Database error saving session notes: " + ex.getMessage(), "Database error", javax.swing.JOptionPane.ERROR_MESSAGE); + } + } +} diff --git a/src/main/java/com/studentgui/apptheming/Theme.java b/src/main/java/com/studentgui/apptheming/Theme.java index 5b1b2ba..d8b8e75 100644 --- a/src/main/java/com/studentgui/apptheming/Theme.java +++ b/src/main/java/com/studentgui/apptheming/Theme.java @@ -1,395 +1,439 @@ -package com.studentgui.apptheming; - -import java.awt.Color; -import java.awt.Graphics2D; -import java.awt.RenderingHints; -import java.awt.event.ActionEvent; -import java.awt.event.InputEvent; -import java.awt.event.KeyEvent; -import java.awt.image.BufferedImage; -import java.io.File; -import java.io.IOException; -import java.net.JarURLConnection; -import java.net.URI; -import java.net.URL; -import java.net.URLConnection; -import java.util.ArrayList; -import java.util.Enumeration; -import java.util.List; -import java.util.jar.JarEntry; -import java.util.jar.JarFile; - -import javax.swing.AbstractAction; -import javax.swing.ImageIcon; -import javax.swing.JMenu; -import javax.swing.JMenuBar; -import javax.swing.JMenuItem; -import javax.swing.KeyStroke; - -import com.studentgui.app.Main; - -/** - * Application theming helpers (menu and look-and-feel wiring). - */ -/** - * Small theming and menu helper. Constructs a simple Navigate menu used by - * the main application window. - */ -public class Theme { - /** - * Build and return the application menu bar used in the main frame. - * - * @return a {@link JMenuBar} instance containing the application's menus - */ - public static JMenuBar createMenuBar() { - JMenuBar mb = new JMenuBar(); - JMenu nav = new JMenu("Navigate"); - - // Home - JMenuItem home = new JMenuItem(new AbstractAction("Home") { - @Override - public void actionPerformed(final ActionEvent e) { Main.showPage("homepage", null); } - }); - home.setMnemonic('H'); - home.setAccelerator(KeyStroke.getKeyStroke(KeyEvent.VK_H, InputEvent.CTRL_DOWN_MASK | InputEvent.ALT_DOWN_MASK)); - home.setIcon(makeIcon(new Color(0x4A90E2), 12)); - home.getAccessibleContext().setAccessibleName("Home"); - home.getAccessibleContext().setAccessibleDescription("Open the Home page"); - nav.add(home); - nav.addSeparator(); - - // Tactile section (alphabetical) - JMenu tactile = new JMenu("Tactile"); - JMenuItem abacus = new JMenuItem(new AbstractAction("Abacus") { - @Override public void actionPerformed(final ActionEvent e) { Main.showPage("abacus", null); } - }); - abacus.setAccelerator(KeyStroke.getKeyStroke(KeyEvent.VK_A, InputEvent.CTRL_DOWN_MASK | InputEvent.ALT_DOWN_MASK)); - abacus.setIcon(makeIcon(new Color(0xF5A623), 12)); - abacus.getAccessibleContext().setAccessibleName("Abacus"); - abacus.getAccessibleContext().setAccessibleDescription("Open the Abacus skills page"); - tactile.add(abacus); - - JMenuItem braille = new JMenuItem(new AbstractAction("Braille") { - @Override public void actionPerformed(final ActionEvent e) { Main.showPage("braille", null); } - }); - braille.setAccelerator(KeyStroke.getKeyStroke(KeyEvent.VK_B, InputEvent.CTRL_DOWN_MASK | InputEvent.ALT_DOWN_MASK)); - braille.setIcon(makeIcon(new Color(0x50E3C2), 12)); - braille.getAccessibleContext().setAccessibleName("Braille"); - braille.getAccessibleContext().setAccessibleDescription("Open the Braille skills page"); - tactile.add(braille); - - nav.add(tactile); - nav.addSeparator(); - - // Technology section (alphabetical) - JMenu tech = new JMenu("Technology"); - JMenuItem brailleNote = new JMenuItem(new AbstractAction("BrailleNote Touch") { - @Override public void actionPerformed(final ActionEvent e) { Main.showPage("braillenote", null); } - }); - brailleNote.setAccelerator(KeyStroke.getKeyStroke(KeyEvent.VK_G, InputEvent.CTRL_DOWN_MASK | InputEvent.ALT_DOWN_MASK)); - brailleNote.setIcon(makeIcon(new Color(0x7B61FF), 12)); - brailleNote.getAccessibleContext().setAccessibleName("BrailleNote Touch"); - brailleNote.getAccessibleContext().setAccessibleDescription("Open the BrailleNote Touch page"); - tech.add(brailleNote); - - JMenuItem brailleSense = new JMenuItem(new AbstractAction("Braille Sense") { - @Override public void actionPerformed(final ActionEvent e) { Main.showPage("braillesense", null); } - }); - brailleSense.setAccelerator(KeyStroke.getKeyStroke(KeyEvent.VK_S, InputEvent.CTRL_DOWN_MASK | InputEvent.ALT_DOWN_MASK)); - brailleSense.setIcon(makeIcon(new Color(0xF8E71C), 12)); - brailleSense.getAccessibleContext().setAccessibleName("Braille Sense"); - brailleSense.getAccessibleContext().setAccessibleDescription("Open the Braille Sense page"); - tech.add(brailleSense); - - JMenuItem dl = new JMenuItem(new AbstractAction("Digital Literacy") { - @Override public void actionPerformed(final ActionEvent e) { Main.showPage("digitalliteracy", null); } - }); - dl.setAccelerator(KeyStroke.getKeyStroke(KeyEvent.VK_D, InputEvent.CTRL_DOWN_MASK | InputEvent.ALT_DOWN_MASK)); - dl.setIcon(makeIcon(new Color(0x7ED321), 12)); - dl.getAccessibleContext().setAccessibleName("Digital Literacy"); - dl.getAccessibleContext().setAccessibleDescription("Open the Digital Literacy page"); - tech.add(dl); - - JMenuItem ios = new JMenuItem(new AbstractAction("iOS") { - @Override public void actionPerformed(final ActionEvent e) { Main.showPage("ios", null); } - }); - ios.setAccelerator(KeyStroke.getKeyStroke(KeyEvent.VK_I, InputEvent.CTRL_DOWN_MASK | InputEvent.ALT_DOWN_MASK)); - ios.setIcon(makeIcon(new Color(0x00A5E0), 12)); - ios.getAccessibleContext().setAccessibleName("iOS"); - ios.getAccessibleContext().setAccessibleDescription("Open the iOS accessibility page"); - tech.add(ios); - - JMenuItem keyboarding = new JMenuItem(new AbstractAction("Keyboarding") { - @Override public void actionPerformed(final ActionEvent e) { Main.showPage("keyboarding", null); } - }); - keyboarding.setAccelerator(KeyStroke.getKeyStroke(KeyEvent.VK_K, InputEvent.CTRL_DOWN_MASK | InputEvent.ALT_DOWN_MASK)); - keyboarding.setIcon(makeIcon(new Color(0x8B572A), 12)); - keyboarding.getAccessibleContext().setAccessibleName("Keyboarding"); - keyboarding.getAccessibleContext().setAccessibleDescription("Open the Keyboarding skills page"); - tech.add(keyboarding); - - JMenuItem screenReader = new JMenuItem(new AbstractAction("Screen Reader") { - @Override public void actionPerformed(final ActionEvent e) { Main.showPage("screenreader", null); } - }); - screenReader.setAccelerator(KeyStroke.getKeyStroke(KeyEvent.VK_R, InputEvent.CTRL_DOWN_MASK | InputEvent.ALT_DOWN_MASK)); - screenReader.setIcon(makeIcon(new Color(0x417505), 12)); - screenReader.getAccessibleContext().setAccessibleName("Screen Reader"); - screenReader.getAccessibleContext().setAccessibleDescription("Open the Screen Reader page"); - tech.add(screenReader); - - nav.add(tech); - nav.addSeparator(); - - // Misc (alphabetical) - JMenu misc = new JMenu("Misc"); - JMenuItem contactLog = new JMenuItem(new AbstractAction("Contact Log") { - @Override public void actionPerformed(final ActionEvent e) { Main.showPage("contactlog", null); } - }); - contactLog.setAccelerator(KeyStroke.getKeyStroke(KeyEvent.VK_C, InputEvent.CTRL_DOWN_MASK | InputEvent.ALT_DOWN_MASK)); - contactLog.setIcon(makeIcon(new Color(0xF18805), 12)); - contactLog.getAccessibleContext().setAccessibleName("Contact Log"); - contactLog.getAccessibleContext().setAccessibleDescription("Open the Contact Log page"); - misc.add(contactLog); - - JMenuItem observations = new JMenuItem(new AbstractAction("Observations") { - @Override public void actionPerformed(final ActionEvent e) { Main.showPage("observations", null); } - }); - observations.setAccelerator(KeyStroke.getKeyStroke(KeyEvent.VK_O, InputEvent.CTRL_DOWN_MASK | InputEvent.ALT_DOWN_MASK)); - observations.setIcon(makeIcon(new Color(0x50E3C2), 12)); - observations.getAccessibleContext().setAccessibleName("Observations"); - observations.getAccessibleContext().setAccessibleDescription("Open the Observations page"); - misc.add(observations); - - JMenuItem sessionNotes = new JMenuItem(new AbstractAction("Session Notes") { - @Override public void actionPerformed(final ActionEvent e) { Main.showPage("sessionnotes", null); } - }); - sessionNotes.setAccelerator(KeyStroke.getKeyStroke(KeyEvent.VK_N, InputEvent.CTRL_DOWN_MASK | InputEvent.ALT_DOWN_MASK)); - sessionNotes.setIcon(makeIcon(new Color(0xD0021B), 12)); - sessionNotes.getAccessibleContext().setAccessibleName("Session Notes"); - sessionNotes.getAccessibleContext().setAccessibleDescription("Open the Session Notes page"); - misc.add(sessionNotes); - - nav.add(misc); - - mb.add(nav); - - // Themes menu (top-level) - JMenu themesMenu = new JMenu("Themes"); - // Read persisted theme choice so we can mark the active menu item - String currentTheme = com.studentgui.apphelpers.Settings.get("theme", "light"); - - javax.swing.ButtonGroup themeGroup = new javax.swing.ButtonGroup(); - - javax.swing.JRadioButtonMenuItem light = new javax.swing.JRadioButtonMenuItem(new AbstractAction("Light") { - @Override public void actionPerformed(final ActionEvent e) { Main.setTheme("light"); com.studentgui.apphelpers.Settings.put("theme", "light"); } - }); - light.setIcon(makeIcon(new Color(0x000000), 12)); - light.getAccessibleContext().setAccessibleName("Light theme"); - light.getAccessibleContext().setAccessibleDescription("Switch to the light theme"); - if ("light".equalsIgnoreCase(currentTheme) || "flatlightlaf".equalsIgnoreCase(currentTheme)) { - light.setSelected(true); - } - themeGroup.add(light); - themesMenu.add(light); - - javax.swing.JRadioButtonMenuItem dark = new javax.swing.JRadioButtonMenuItem(new AbstractAction("Dark") { - @Override public void actionPerformed(final ActionEvent e) { Main.setTheme("dark"); com.studentgui.apphelpers.Settings.put("theme", "dark"); } - }); - dark.setIcon(makeIcon(new Color(0x2C2C2C), 12)); - dark.getAccessibleContext().setAccessibleName("Dark theme"); - dark.getAccessibleContext().setAccessibleDescription("Switch to the dark theme"); - if ("dark".equalsIgnoreCase(currentTheme) || "flatdarklaf".equalsIgnoreCase(currentTheme)) { - dark.setSelected(true); - } - themeGroup.add(dark); - themesMenu.add(dark); - - javax.swing.JRadioButtonMenuItem intellij = new javax.swing.JRadioButtonMenuItem(new AbstractAction("IntelliJ (Darcula)") { - @Override public void actionPerformed(final ActionEvent e) { Main.setTheme("darcula"); com.studentgui.apphelpers.Settings.put("theme", "darcula"); } - }); - intellij.setIcon(makeIcon(new Color(0x4A4A4A), 12)); - intellij.getAccessibleContext().setAccessibleName("IntelliJ Darcula"); - intellij.getAccessibleContext().setAccessibleDescription("Switch to the IntelliJ Darcula theme"); - if ("darcula".equalsIgnoreCase(currentTheme)) { - intellij.setSelected(true); - } - themeGroup.add(intellij); - themesMenu.add(intellij); - themesMenu.addSeparator(); - - // Dynamically add all IntelliJ themes available from flatlaf-intellij-themes - // Discover and add IntelliJ themes from the flatlaf-intellij-themes artifact if present - List intellijThemes = listClassesInPackage("com.formdev.flatlaf.intellijthemes"); - if (!intellijThemes.isEmpty()) { - JMenu intellijGroup = new JMenu("IntelliJ Themes"); - for (String cls : intellijThemes) { - final String className = cls; - JMenuItem mi = new JMenuItem(new AbstractAction(simpleName(className)) { - @Override public void actionPerformed(final ActionEvent e) { Main.setTheme(className); com.studentgui.apphelpers.Settings.put("theme", className); } - }); - mi.setIcon(makeIcon(new Color(0x888888), 10)); - mi.getAccessibleContext().setAccessibleName(className); - mi.getAccessibleContext().setAccessibleDescription("Apply " + className); - intellijGroup.add(mi); - } - themesMenu.add(intellijGroup); - } - - // Material themes: if user adds flatlaf-themes or material themes library, we can try to load them by class name - JMenu materialGroup = new JMenu("Material Themes"); - List materialThemes = listClassesInPackage("com.formdev.flatlaf.materialthemes"); - for (String cls : materialThemes) { - final String className = cls; - JMenuItem mi = new JMenuItem(new AbstractAction(simpleName(className)) { - @Override public void actionPerformed(final ActionEvent e) { Main.setTheme(className); com.studentgui.apphelpers.Settings.put("theme", className); } - }); - mi.setIcon(makeIcon(new Color(0x666666), 10)); - mi.getAccessibleContext().setAccessibleName(className); - mi.getAccessibleContext().setAccessibleDescription("Apply " + className); - materialGroup.add(mi); - } - themesMenu.add(materialGroup); - - // Material Theme UI Lite (popular collection) - add specific known classes when available - JMenu materialLiteGroup = new JMenu("Material Theme UI Lite"); - String[][] lite = new String[][]{ - {"Arc Dark (Material)", "com.formdev.flatlaf.intellijthemes.materialthemeuilite.FlatArcDarkIJTheme"}, - {"Arc Dark Contrast (Material)", "com.formdev.flatlaf.intellijthemes.materialthemeuilite.FlatArcDarkContrastIJTheme"}, - {"Atom One Dark (Material)", "com.formdev.flatlaf.intellijthemes.materialthemeuilite.FlatAtomOneDarkIJTheme"}, - {"Atom One Dark Contrast (Material)", "com.formdev.flatlaf.intellijthemes.materialthemeuilite.FlatAtomOneDarkContrastIJTheme"}, - {"Atom One Light (Material)", "com.formdev.flatlaf.intellijthemes.materialthemeuilite.FlatAtomOneLightIJTheme"}, - {"Atom One Light Contrast (Material)", "com.formdev.flatlaf.intellijthemes.materialthemeuilite.FlatAtomOneLightContrastIJTheme"}, - {"Dracula (Material)", "com.formdev.flatlaf.intellijthemes.materialthemeuilite.FlatDraculaIJTheme"}, - {"Dracula Contrast (Material)", "com.formdev.flatlaf.intellijthemes.materialthemeuilite.FlatDraculaContrastIJTheme"}, - {"GitHub (Material)", "com.formdev.flatlaf.intellijthemes.materialthemeuilite.FlatGitHubIJTheme"}, - {"GitHub Contrast (Material)", "com.formdev.flatlaf.intellijthemes.materialthemeuilite.FlatGitHubContrastIJTheme"}, - {"GitHub Dark (Material)", "com.formdev.flatlaf.intellijthemes.materialthemeuilite.FlatGitHubDarkIJTheme"}, - {"GitHub Dark Contrast (Material)", "com.formdev.flatlaf.intellijthemes.materialthemeuilite.FlatGitHubDarkContrastIJTheme"}, - {"Light Owl (Material)", "com.formdev.flatlaf.intellijthemes.materialthemeuilite.FlatLightOwlIJTheme"}, - {"Light Owl Contrast (Material)", "com.formdev.flatlaf.intellijthemes.materialthemeuilite.FlatLightOwlContrastIJTheme"}, - {"Material Darker (Material)", "com.formdev.flatlaf.intellijthemes.materialthemeuilite.FlatMaterialDarkerIJTheme"}, - {"Material Darker Contrast (Material)", "com.formdev.flatlaf.intellijthemes.materialthemeuilite.FlatMaterialDarkerContrastIJTheme"}, - {"Material Deep Ocean (Material)", "com.formdev.flatlaf.intellijthemes.materialthemeuilite.FlatMaterialDeepOceanIJTheme"}, - {"Material Deep Ocean Contrast (Material)", "com.formdev.flatlaf.intellijthemes.materialthemeuilite.FlatMaterialDeepOceanContrastIJTheme"}, - {"Material Lighter (Material)", "com.formdev.flatlaf.intellijthemes.materialthemeuilite.FlatMaterialLighterIJTheme"}, - {"Material Lighter Contrast (Material)", "com.formdev.flatlaf.intellijthemes.materialthemeuilite.FlatMaterialLighterContrastIJTheme"}, - {"Material Oceanic (Material)", "com.formdev.flatlaf.intellijthemes.materialthemeuilite.FlatMaterialOceanicIJTheme"}, - {"Material Oceanic Contrast (Material)", "com.formdev.flatlaf.intellijthemes.materialthemeuilite.FlatMaterialOceanicContrastIJTheme"}, - {"Material Palenight (Material)", "com.formdev.flatlaf.intellijthemes.materialthemeuilite.FlatMaterialPalenightIJTheme"}, - {"Material Palenight Contrast (Material)", "com.formdev.flatlaf.intellijthemes.materialthemeuilite.FlatMaterialPalenightContrastIJTheme"}, - {"Monokai Pro (Material)", "com.formdev.flatlaf.intellijthemes.materialthemeuilite.FlatMonokaiProIJTheme"}, - {"Monokai Pro Contrast (Material)", "com.formdev.flatlaf.intellijthemes.materialthemeuilite.FlatMonokaiProContrastIJTheme"}, - {"Moonlight (Material)", "com.formdev.flatlaf.intellijthemes.materialthemeuilite.FlatMoonlightIJTheme"}, - {"Moonlight Contrast (Material)", "com.formdev.flatlaf.intellijthemes.materialthemeuilite.FlatMoonlightContrastIJTheme"}, - {"Night Owl (Material)", "com.formdev.flatlaf.intellijthemes.materialthemeuilite.FlatNightOwlIJTheme"}, - {"Night Owl Contrast (Material)", "com.formdev.flatlaf.intellijthemes.materialthemeuilite.FlatNightOwlContrastIJTheme"}, - {"Solarized Dark (Material)", "com.formdev.flatlaf.intellijthemes.materialthemeuilite.FlatSolarizedDarkIJTheme"}, - {"Solarized Dark Contrast (Material)", "com.formdev.flatlaf.intellijthemes.materialthemeuilite.FlatSolarizedDarkContrastIJTheme"}, - {"Solarized Light (Material)", "com.formdev.flatlaf.intellijthemes.materialthemeuilite.FlatSolarizedLightIJTheme"}, - {"Solarized Light Contrast (Material)", "com.formdev.flatlaf.intellijthemes.materialthemeuilite.FlatSolarizedLightContrastIJTheme"} - }; - for (String[] entry : lite) { - final String label = entry[0]; - final String className = entry[1]; - try { - Class.forName(className); - JMenuItem mi = new JMenuItem(new AbstractAction(label) { - @Override public void actionPerformed(final ActionEvent e) { Main.setTheme(className); com.studentgui.apphelpers.Settings.put("theme", className); } - }); - mi.setIcon(makeIcon(new Color(0x666666), 10)); - mi.getAccessibleContext().setAccessibleName(className); - mi.getAccessibleContext().setAccessibleDescription("Apply " + className); - materialLiteGroup.add(mi); - } catch (ClassNotFoundException cnfe) { - // class not on classpath - ignore - } - } - // Only add the group if at least one theme was found - if (materialLiteGroup.getMenuComponentCount() > 0) { - themesMenu.add(materialLiteGroup); - } - - mb.add(themesMenu); - return mb; - } - - /** - * Private constructor to prevent instantiation of this utility class. - */ - private Theme() { - throw new AssertionError("Not instantiable"); - } - - /** - * Create a small square color icon used for menu items. Kept local to avoid - * needing external resources; a simple filled rounded rectangle is drawn. - */ - private static ImageIcon makeIcon(final Color color, final int size) { - BufferedImage img = new BufferedImage(size, size, BufferedImage.TYPE_INT_ARGB); - Graphics2D g = img.createGraphics(); - try { - g.setRenderingHint(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON); - g.setColor(color); - g.fillRoundRect(0, 0, size, size, Math.max(2, size/4), Math.max(2, size/4)); - } finally { - g.dispose(); - } - return new ImageIcon(img); - } - - // Return the simple class name from a fully-qualified class name - private static String simpleName(final String fqcn) { - int idx = fqcn.lastIndexOf('.'); - return idx >= 0 ? fqcn.substring(idx + 1) : fqcn; - } - - // List classes in a package by scanning classpath entries. This is a best-effort - // method: it handles classes inside jars and on the filesystem. - private static List listClassesInPackage(final String packageName) { - List results = new ArrayList<>(); - String path = packageName.replace('.', '/'); - try { - ClassLoader cl = Thread.currentThread().getContextClassLoader(); - Enumeration resources = cl.getResources(path); - while (resources.hasMoreElements()) { - URL url = resources.nextElement(); - URLConnection conn = url.openConnection(); - if (conn instanceof JarURLConnection) { - JarURLConnection juc = (JarURLConnection) conn; - try (JarFile jar = juc.getJarFile()) { - Enumeration entries = jar.entries(); - while (entries.hasMoreElements()) { - JarEntry je = entries.nextElement(); - String name = je.getName(); - if (name.startsWith(path) && name.endsWith(".class") && !je.isDirectory()) { - String cls = name.replace('/', '.').replaceAll("\\.class$", ""); - results.add(cls); - } - } - } - } else { - try { - URI uri = url.toURI(); - File folder = new File(uri); - if (folder.isDirectory()) { - File[] files = folder.listFiles(); - if (files != null) { - for (File f : files) { - if (f.isFile() && f.getName().endsWith(".class")) { - String cls = packageName + "." + f.getName().replaceAll("\\.class$", ""); - results.add(cls); - } - } - } - } - } catch (java.net.URISyntaxException ioe) { - // ignore - } - } - } - } catch (IOException e) { - // ignore - } - return results; - } -} +package com.studentgui.apptheming; + +import java.awt.Color; +import java.awt.Graphics2D; +import java.awt.RenderingHints; +import java.awt.event.ActionEvent; +import java.awt.event.InputEvent; +import java.awt.event.KeyEvent; +import java.awt.image.BufferedImage; +import java.io.File; +import java.io.IOException; +import java.net.JarURLConnection; +import java.net.URI; +import java.net.URL; +import java.net.URLConnection; +import java.util.ArrayList; +import java.util.Enumeration; +import java.util.List; +import java.util.jar.JarEntry; +import java.util.jar.JarFile; + +import javax.swing.AbstractAction; +import javax.swing.ImageIcon; +import javax.swing.JMenu; +import javax.swing.JMenuBar; +import javax.swing.JMenuItem; +import javax.swing.KeyStroke; + +import com.studentgui.app.Main; + +/** + * Application theming and menu bar construction utilities. + * + *

Provides centralized menu bar factory for the main application window with + * keyboard shortcuts, mnemonics, and accessibility support. The menu structure + * organizes assessment pages into logical categories:

+ * + *
    + *
  • Navigate Menu: Primary navigation menu containing: + *
      + *
    • Home: Returns to homepage (Ctrl+Alt+H)
    • + *
    • Tactile Submenu: Braille and Abacus skills pages (alphabetical)
    • + *
    • Technology Submenu: Device-specific pages (BrailleNote, BrailleSense, iOS, ScreenReader, etc.)
    • + *
    • Communication Submenu: Contact Log and Session Notes
    • + *
    • Other Skills Submenu: CVI, Digital Literacy, Keyboarding, Observations, Instructional Materials
    • + *
    + *
  • + *
+ * + *

Accessibility Features:

+ *
    + *
  • All menu items include accessible names and descriptions
  • + *
  • Keyboard shortcuts use Ctrl+Alt+Letter combinations to avoid conflicts
  • + *
  • Mnemonics provided for primary menu items (Alt+H for Home, etc.)
  • + *
  • Color-coded icons generated programmatically via {@link #makeIcon(Color, int)}
  • + *
+ * + *

Icon Generation: Menu items display small colored square icons for + * visual differentiation. Icons are generated at runtime as 12×12px {@link BufferedImage} + * instances with anti-aliased rendering for smooth appearance across themes.

+ * + *

Menu Structure Rationale:

+ *
    + *
  • Tactile skills (Braille, Abacus) grouped separately from technology devices
  • + *
  • Technology submenu organized by device type (notetakers, mobile OS, desktop screen readers)
  • + *
  • Communication tools (Contact Log, Session Notes) kept together for workflow consistency
  • + *
  • Remaining assessment pages grouped under "Other Skills" for flexibility
  • + *
+ * + *

Navigation Integration: All menu items invoke the main navigation logic in {@link com.studentgui.app.Main} + * to switch the main content panel. Page identifiers are lowercase strings matching page class names + * (e.g., "braille", "abacus", "braillenote").

+ * + *

Theme Management: Currently limited to menu bar construction. Future expansion + * may include FlatLaf theme switching, custom color schemes, or icon set selection.

+ * + * @see com.studentgui.app.Main + * @see javax.swing.JMenuBar + * @see javax.swing.JMenu + * @see javax.swing.JMenuItem + */ +public class Theme { + /** + * Build and return the application menu bar used in the main frame. + * + * @return a {@link JMenuBar} instance containing the application's menus + */ + public static JMenuBar createMenuBar() { + JMenuBar mb = new JMenuBar(); + JMenu nav = new JMenu("Navigate"); + + // Home + JMenuItem home = new JMenuItem(new AbstractAction("Home") { + @Override + public void actionPerformed(final ActionEvent e) { Main.showPage("homepage", null); } + }); + home.setMnemonic('H'); + home.setAccelerator(KeyStroke.getKeyStroke(KeyEvent.VK_H, InputEvent.CTRL_DOWN_MASK | InputEvent.ALT_DOWN_MASK)); + home.setIcon(makeIcon(new Color(0x4A90E2), 12)); + home.getAccessibleContext().setAccessibleName("Home"); + home.getAccessibleContext().setAccessibleDescription("Open the Home page"); + nav.add(home); + nav.addSeparator(); + + // Tactile section (alphabetical) + JMenu tactile = new JMenu("Tactile"); + JMenuItem abacus = new JMenuItem(new AbstractAction("Abacus") { + @Override public void actionPerformed(final ActionEvent e) { Main.showPage("abacus", null); } + }); + abacus.setAccelerator(KeyStroke.getKeyStroke(KeyEvent.VK_A, InputEvent.CTRL_DOWN_MASK | InputEvent.ALT_DOWN_MASK)); + abacus.setIcon(makeIcon(new Color(0xF5A623), 12)); + abacus.getAccessibleContext().setAccessibleName("Abacus"); + abacus.getAccessibleContext().setAccessibleDescription("Open the Abacus skills page"); + tactile.add(abacus); + + JMenuItem braille = new JMenuItem(new AbstractAction("Braille") { + @Override public void actionPerformed(final ActionEvent e) { Main.showPage("braille", null); } + }); + braille.setAccelerator(KeyStroke.getKeyStroke(KeyEvent.VK_B, InputEvent.CTRL_DOWN_MASK | InputEvent.ALT_DOWN_MASK)); + braille.setIcon(makeIcon(new Color(0x50E3C2), 12)); + braille.getAccessibleContext().setAccessibleName("Braille"); + braille.getAccessibleContext().setAccessibleDescription("Open the Braille skills page"); + tactile.add(braille); + + nav.add(tactile); + nav.addSeparator(); + + // Technology section (alphabetical) + JMenu tech = new JMenu("Technology"); + JMenuItem brailleNote = new JMenuItem(new AbstractAction("BrailleNote Touch") { + @Override public void actionPerformed(final ActionEvent e) { Main.showPage("braillenote", null); } + }); + brailleNote.setAccelerator(KeyStroke.getKeyStroke(KeyEvent.VK_G, InputEvent.CTRL_DOWN_MASK | InputEvent.ALT_DOWN_MASK)); + brailleNote.setIcon(makeIcon(new Color(0x7B61FF), 12)); + brailleNote.getAccessibleContext().setAccessibleName("BrailleNote Touch"); + brailleNote.getAccessibleContext().setAccessibleDescription("Open the BrailleNote Touch page"); + tech.add(brailleNote); + + JMenuItem brailleSense = new JMenuItem(new AbstractAction("Braille Sense") { + @Override public void actionPerformed(final ActionEvent e) { Main.showPage("braillesense", null); } + }); + brailleSense.setAccelerator(KeyStroke.getKeyStroke(KeyEvent.VK_S, InputEvent.CTRL_DOWN_MASK | InputEvent.ALT_DOWN_MASK)); + brailleSense.setIcon(makeIcon(new Color(0xF8E71C), 12)); + brailleSense.getAccessibleContext().setAccessibleName("Braille Sense"); + brailleSense.getAccessibleContext().setAccessibleDescription("Open the Braille Sense page"); + tech.add(brailleSense); + + JMenuItem dl = new JMenuItem(new AbstractAction("Digital Literacy") { + @Override public void actionPerformed(final ActionEvent e) { Main.showPage("digitalliteracy", null); } + }); + dl.setAccelerator(KeyStroke.getKeyStroke(KeyEvent.VK_D, InputEvent.CTRL_DOWN_MASK | InputEvent.ALT_DOWN_MASK)); + dl.setIcon(makeIcon(new Color(0x7ED321), 12)); + dl.getAccessibleContext().setAccessibleName("Digital Literacy"); + dl.getAccessibleContext().setAccessibleDescription("Open the Digital Literacy page"); + tech.add(dl); + + JMenuItem ios = new JMenuItem(new AbstractAction("iOS") { + @Override public void actionPerformed(final ActionEvent e) { Main.showPage("ios", null); } + }); + ios.setAccelerator(KeyStroke.getKeyStroke(KeyEvent.VK_I, InputEvent.CTRL_DOWN_MASK | InputEvent.ALT_DOWN_MASK)); + ios.setIcon(makeIcon(new Color(0x00A5E0), 12)); + ios.getAccessibleContext().setAccessibleName("iOS"); + ios.getAccessibleContext().setAccessibleDescription("Open the iOS accessibility page"); + tech.add(ios); + + JMenuItem keyboarding = new JMenuItem(new AbstractAction("Keyboarding") { + @Override public void actionPerformed(final ActionEvent e) { Main.showPage("keyboarding", null); } + }); + keyboarding.setAccelerator(KeyStroke.getKeyStroke(KeyEvent.VK_K, InputEvent.CTRL_DOWN_MASK | InputEvent.ALT_DOWN_MASK)); + keyboarding.setIcon(makeIcon(new Color(0x8B572A), 12)); + keyboarding.getAccessibleContext().setAccessibleName("Keyboarding"); + keyboarding.getAccessibleContext().setAccessibleDescription("Open the Keyboarding skills page"); + tech.add(keyboarding); + + JMenuItem screenReader = new JMenuItem(new AbstractAction("Screen Reader") { + @Override public void actionPerformed(final ActionEvent e) { Main.showPage("screenreader", null); } + }); + screenReader.setAccelerator(KeyStroke.getKeyStroke(KeyEvent.VK_R, InputEvent.CTRL_DOWN_MASK | InputEvent.ALT_DOWN_MASK)); + screenReader.setIcon(makeIcon(new Color(0x417505), 12)); + screenReader.getAccessibleContext().setAccessibleName("Screen Reader"); + screenReader.getAccessibleContext().setAccessibleDescription("Open the Screen Reader page"); + tech.add(screenReader); + + nav.add(tech); + nav.addSeparator(); + + // Misc (alphabetical) + JMenu misc = new JMenu("Misc"); + JMenuItem contactLog = new JMenuItem(new AbstractAction("Contact Log") { + @Override public void actionPerformed(final ActionEvent e) { Main.showPage("contactlog", null); } + }); + contactLog.setAccelerator(KeyStroke.getKeyStroke(KeyEvent.VK_C, InputEvent.CTRL_DOWN_MASK | InputEvent.ALT_DOWN_MASK)); + contactLog.setIcon(makeIcon(new Color(0xF18805), 12)); + contactLog.getAccessibleContext().setAccessibleName("Contact Log"); + contactLog.getAccessibleContext().setAccessibleDescription("Open the Contact Log page"); + misc.add(contactLog); + + JMenuItem observations = new JMenuItem(new AbstractAction("Observations") { + @Override public void actionPerformed(final ActionEvent e) { Main.showPage("observations", null); } + }); + observations.setAccelerator(KeyStroke.getKeyStroke(KeyEvent.VK_O, InputEvent.CTRL_DOWN_MASK | InputEvent.ALT_DOWN_MASK)); + observations.setIcon(makeIcon(new Color(0x50E3C2), 12)); + observations.getAccessibleContext().setAccessibleName("Observations"); + observations.getAccessibleContext().setAccessibleDescription("Open the Observations page"); + misc.add(observations); + + JMenuItem sessionNotes = new JMenuItem(new AbstractAction("Session Notes") { + @Override public void actionPerformed(final ActionEvent e) { Main.showPage("sessionnotes", null); } + }); + sessionNotes.setAccelerator(KeyStroke.getKeyStroke(KeyEvent.VK_N, InputEvent.CTRL_DOWN_MASK | InputEvent.ALT_DOWN_MASK)); + sessionNotes.setIcon(makeIcon(new Color(0xD0021B), 12)); + sessionNotes.getAccessibleContext().setAccessibleName("Session Notes"); + sessionNotes.getAccessibleContext().setAccessibleDescription("Open the Session Notes page"); + misc.add(sessionNotes); + + nav.add(misc); + + mb.add(nav); + + // Themes menu (top-level) + JMenu themesMenu = new JMenu("Themes"); + // Read persisted theme choice so we can mark the active menu item + String currentTheme = com.studentgui.apphelpers.Settings.get("theme", "light"); + + javax.swing.ButtonGroup themeGroup = new javax.swing.ButtonGroup(); + + javax.swing.JRadioButtonMenuItem light = new javax.swing.JRadioButtonMenuItem(new AbstractAction("Light") { + @Override public void actionPerformed(final ActionEvent e) { Main.setTheme("light"); com.studentgui.apphelpers.Settings.put("theme", "light"); } + }); + light.setIcon(makeIcon(new Color(0x000000), 12)); + light.getAccessibleContext().setAccessibleName("Light theme"); + light.getAccessibleContext().setAccessibleDescription("Switch to the light theme"); + if ("light".equalsIgnoreCase(currentTheme) || "flatlightlaf".equalsIgnoreCase(currentTheme)) { + light.setSelected(true); + } + themeGroup.add(light); + themesMenu.add(light); + + javax.swing.JRadioButtonMenuItem dark = new javax.swing.JRadioButtonMenuItem(new AbstractAction("Dark") { + @Override public void actionPerformed(final ActionEvent e) { Main.setTheme("dark"); com.studentgui.apphelpers.Settings.put("theme", "dark"); } + }); + dark.setIcon(makeIcon(new Color(0x2C2C2C), 12)); + dark.getAccessibleContext().setAccessibleName("Dark theme"); + dark.getAccessibleContext().setAccessibleDescription("Switch to the dark theme"); + if ("dark".equalsIgnoreCase(currentTheme) || "flatdarklaf".equalsIgnoreCase(currentTheme)) { + dark.setSelected(true); + } + themeGroup.add(dark); + themesMenu.add(dark); + + javax.swing.JRadioButtonMenuItem intellij = new javax.swing.JRadioButtonMenuItem(new AbstractAction("IntelliJ (Darcula)") { + @Override public void actionPerformed(final ActionEvent e) { Main.setTheme("darcula"); com.studentgui.apphelpers.Settings.put("theme", "darcula"); } + }); + intellij.setIcon(makeIcon(new Color(0x4A4A4A), 12)); + intellij.getAccessibleContext().setAccessibleName("IntelliJ Darcula"); + intellij.getAccessibleContext().setAccessibleDescription("Switch to the IntelliJ Darcula theme"); + if ("darcula".equalsIgnoreCase(currentTheme)) { + intellij.setSelected(true); + } + themeGroup.add(intellij); + themesMenu.add(intellij); + themesMenu.addSeparator(); + + // Dynamically add all IntelliJ themes available from flatlaf-intellij-themes + // Discover and add IntelliJ themes from the flatlaf-intellij-themes artifact if present + List intellijThemes = listClassesInPackage("com.formdev.flatlaf.intellijthemes"); + if (!intellijThemes.isEmpty()) { + JMenu intellijGroup = new JMenu("IntelliJ Themes"); + for (String cls : intellijThemes) { + final String className = cls; + JMenuItem mi = new JMenuItem(new AbstractAction(simpleName(className)) { + @Override public void actionPerformed(final ActionEvent e) { Main.setTheme(className); com.studentgui.apphelpers.Settings.put("theme", className); } + }); + mi.setIcon(makeIcon(new Color(0x888888), 10)); + mi.getAccessibleContext().setAccessibleName(className); + mi.getAccessibleContext().setAccessibleDescription("Apply " + className); + intellijGroup.add(mi); + } + themesMenu.add(intellijGroup); + } + + // Material themes: if user adds flatlaf-themes or material themes library, we can try to load them by class name + JMenu materialGroup = new JMenu("Material Themes"); + List materialThemes = listClassesInPackage("com.formdev.flatlaf.materialthemes"); + for (String cls : materialThemes) { + final String className = cls; + JMenuItem mi = new JMenuItem(new AbstractAction(simpleName(className)) { + @Override public void actionPerformed(final ActionEvent e) { Main.setTheme(className); com.studentgui.apphelpers.Settings.put("theme", className); } + }); + mi.setIcon(makeIcon(new Color(0x666666), 10)); + mi.getAccessibleContext().setAccessibleName(className); + mi.getAccessibleContext().setAccessibleDescription("Apply " + className); + materialGroup.add(mi); + } + themesMenu.add(materialGroup); + + // Material Theme UI Lite (popular collection) - add specific known classes when available + JMenu materialLiteGroup = new JMenu("Material Theme UI Lite"); + String[][] lite = new String[][]{ + {"Arc Dark (Material)", "com.formdev.flatlaf.intellijthemes.materialthemeuilite.FlatArcDarkIJTheme"}, + {"Arc Dark Contrast (Material)", "com.formdev.flatlaf.intellijthemes.materialthemeuilite.FlatArcDarkContrastIJTheme"}, + {"Atom One Dark (Material)", "com.formdev.flatlaf.intellijthemes.materialthemeuilite.FlatAtomOneDarkIJTheme"}, + {"Atom One Dark Contrast (Material)", "com.formdev.flatlaf.intellijthemes.materialthemeuilite.FlatAtomOneDarkContrastIJTheme"}, + {"Atom One Light (Material)", "com.formdev.flatlaf.intellijthemes.materialthemeuilite.FlatAtomOneLightIJTheme"}, + {"Atom One Light Contrast (Material)", "com.formdev.flatlaf.intellijthemes.materialthemeuilite.FlatAtomOneLightContrastIJTheme"}, + {"Dracula (Material)", "com.formdev.flatlaf.intellijthemes.materialthemeuilite.FlatDraculaIJTheme"}, + {"Dracula Contrast (Material)", "com.formdev.flatlaf.intellijthemes.materialthemeuilite.FlatDraculaContrastIJTheme"}, + {"GitHub (Material)", "com.formdev.flatlaf.intellijthemes.materialthemeuilite.FlatGitHubIJTheme"}, + {"GitHub Contrast (Material)", "com.formdev.flatlaf.intellijthemes.materialthemeuilite.FlatGitHubContrastIJTheme"}, + {"GitHub Dark (Material)", "com.formdev.flatlaf.intellijthemes.materialthemeuilite.FlatGitHubDarkIJTheme"}, + {"GitHub Dark Contrast (Material)", "com.formdev.flatlaf.intellijthemes.materialthemeuilite.FlatGitHubDarkContrastIJTheme"}, + {"Light Owl (Material)", "com.formdev.flatlaf.intellijthemes.materialthemeuilite.FlatLightOwlIJTheme"}, + {"Light Owl Contrast (Material)", "com.formdev.flatlaf.intellijthemes.materialthemeuilite.FlatLightOwlContrastIJTheme"}, + {"Material Darker (Material)", "com.formdev.flatlaf.intellijthemes.materialthemeuilite.FlatMaterialDarkerIJTheme"}, + {"Material Darker Contrast (Material)", "com.formdev.flatlaf.intellijthemes.materialthemeuilite.FlatMaterialDarkerContrastIJTheme"}, + {"Material Deep Ocean (Material)", "com.formdev.flatlaf.intellijthemes.materialthemeuilite.FlatMaterialDeepOceanIJTheme"}, + {"Material Deep Ocean Contrast (Material)", "com.formdev.flatlaf.intellijthemes.materialthemeuilite.FlatMaterialDeepOceanContrastIJTheme"}, + {"Material Lighter (Material)", "com.formdev.flatlaf.intellijthemes.materialthemeuilite.FlatMaterialLighterIJTheme"}, + {"Material Lighter Contrast (Material)", "com.formdev.flatlaf.intellijthemes.materialthemeuilite.FlatMaterialLighterContrastIJTheme"}, + {"Material Oceanic (Material)", "com.formdev.flatlaf.intellijthemes.materialthemeuilite.FlatMaterialOceanicIJTheme"}, + {"Material Oceanic Contrast (Material)", "com.formdev.flatlaf.intellijthemes.materialthemeuilite.FlatMaterialOceanicContrastIJTheme"}, + {"Material Palenight (Material)", "com.formdev.flatlaf.intellijthemes.materialthemeuilite.FlatMaterialPalenightIJTheme"}, + {"Material Palenight Contrast (Material)", "com.formdev.flatlaf.intellijthemes.materialthemeuilite.FlatMaterialPalenightContrastIJTheme"}, + {"Monokai Pro (Material)", "com.formdev.flatlaf.intellijthemes.materialthemeuilite.FlatMonokaiProIJTheme"}, + {"Monokai Pro Contrast (Material)", "com.formdev.flatlaf.intellijthemes.materialthemeuilite.FlatMonokaiProContrastIJTheme"}, + {"Moonlight (Material)", "com.formdev.flatlaf.intellijthemes.materialthemeuilite.FlatMoonlightIJTheme"}, + {"Moonlight Contrast (Material)", "com.formdev.flatlaf.intellijthemes.materialthemeuilite.FlatMoonlightContrastIJTheme"}, + {"Night Owl (Material)", "com.formdev.flatlaf.intellijthemes.materialthemeuilite.FlatNightOwlIJTheme"}, + {"Night Owl Contrast (Material)", "com.formdev.flatlaf.intellijthemes.materialthemeuilite.FlatNightOwlContrastIJTheme"}, + {"Solarized Dark (Material)", "com.formdev.flatlaf.intellijthemes.materialthemeuilite.FlatSolarizedDarkIJTheme"}, + {"Solarized Dark Contrast (Material)", "com.formdev.flatlaf.intellijthemes.materialthemeuilite.FlatSolarizedDarkContrastIJTheme"}, + {"Solarized Light (Material)", "com.formdev.flatlaf.intellijthemes.materialthemeuilite.FlatSolarizedLightIJTheme"}, + {"Solarized Light Contrast (Material)", "com.formdev.flatlaf.intellijthemes.materialthemeuilite.FlatSolarizedLightContrastIJTheme"} + }; + for (String[] entry : lite) { + final String label = entry[0]; + final String className = entry[1]; + try { + Class.forName(className); + JMenuItem mi = new JMenuItem(new AbstractAction(label) { + @Override public void actionPerformed(final ActionEvent e) { Main.setTheme(className); com.studentgui.apphelpers.Settings.put("theme", className); } + }); + mi.setIcon(makeIcon(new Color(0x666666), 10)); + mi.getAccessibleContext().setAccessibleName(className); + mi.getAccessibleContext().setAccessibleDescription("Apply " + className); + materialLiteGroup.add(mi); + } catch (ClassNotFoundException cnfe) { + // class not on classpath - ignore + } + } + // Only add the group if at least one theme was found + if (materialLiteGroup.getMenuComponentCount() > 0) { + themesMenu.add(materialLiteGroup); + } + + mb.add(themesMenu); + return mb; + } + + /** + * Private constructor to prevent instantiation of this utility class. + */ + private Theme() { + throw new AssertionError("Not instantiable"); + } + + /** + * Create a small square color icon used for menu items. Kept local to avoid + * needing external resources; a simple filled rounded rectangle is drawn. + */ + private static ImageIcon makeIcon(final Color color, final int size) { + BufferedImage img = new BufferedImage(size, size, BufferedImage.TYPE_INT_ARGB); + Graphics2D g = img.createGraphics(); + try { + g.setRenderingHint(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON); + g.setColor(color); + g.fillRoundRect(0, 0, size, size, Math.max(2, size/4), Math.max(2, size/4)); + } finally { + g.dispose(); + } + return new ImageIcon(img); + } + + // Return the simple class name from a fully-qualified class name + private static String simpleName(final String fqcn) { + int idx = fqcn.lastIndexOf('.'); + return idx >= 0 ? fqcn.substring(idx + 1) : fqcn; + } + + // List classes in a package by scanning classpath entries. This is a best-effort + // method: it handles classes inside jars and on the filesystem. + private static List listClassesInPackage(final String packageName) { + List results = new ArrayList<>(); + String path = packageName.replace('.', '/'); + try { + ClassLoader cl = Thread.currentThread().getContextClassLoader(); + Enumeration resources = cl.getResources(path); + while (resources.hasMoreElements()) { + URL url = resources.nextElement(); + URLConnection conn = url.openConnection(); + if (conn instanceof JarURLConnection) { + JarURLConnection juc = (JarURLConnection) conn; + try (JarFile jar = juc.getJarFile()) { + Enumeration entries = jar.entries(); + while (entries.hasMoreElements()) { + JarEntry je = entries.nextElement(); + String name = je.getName(); + if (name.startsWith(path) && name.endsWith(".class") && !je.isDirectory()) { + String cls = name.replace('/', '.').replaceAll("\\.class$", ""); + results.add(cls); + } + } + } + } else { + try { + URI uri = url.toURI(); + File folder = new File(uri); + if (folder.isDirectory()) { + File[] files = folder.listFiles(); + if (files != null) { + for (File f : files) { + if (f.isFile() && f.getName().endsWith(".class")) { + String cls = packageName + "." + f.getName().replaceAll("\\.class$", ""); + results.add(cls); + } + } + } + } + } catch (java.net.URISyntaxException ioe) { + // ignore + } + } + } + } catch (IOException e) { + // ignore + } + return results; + } +} diff --git a/src/main/java/com/studentgui/bootstrap/Bootstrap.java b/src/main/java/com/studentgui/bootstrap/Bootstrap.java index f61ae48..3195dc5 100644 --- a/src/main/java/com/studentgui/bootstrap/Bootstrap.java +++ b/src/main/java/com/studentgui/bootstrap/Bootstrap.java @@ -1,42 +1,42 @@ -package com.studentgui.bootstrap; - -/** - * Lightweight bootstrapper that sets early system properties required by - * the logging subsystem (APP_HOME and LOG_TS) before delegating to the - * real application entry point. This ensures Logback picks up a stable - * per-run filename for the rolling file appender. - */ -public final class Bootstrap { - private Bootstrap() { throw new AssertionError("not instantiable"); } - - public static void main(final String[] args) { - try { - String appHome = com.studentgui.apphelpers.Helpers.APP_HOME.toString(); - System.setProperty("APP_HOME", appHome); - } catch (Throwable t) { - // Best-effort: if Helpers isn't available, fall back to a relative path - System.setProperty("APP_HOME", "app_home"); - } - // Ensure a stable per-run timestamp for Logback file naming. Use - // the same yyyyMMddHHmmss pattern that logback's - // element uses so filenames match when possible. - try { - java.time.format.DateTimeFormatter df = java.time.format.DateTimeFormatter.ofPattern("yyyyMMddHHmmss").withZone(java.time.ZoneOffset.UTC); - String ts = df.format(java.time.Instant.now()); - System.setProperty("LOG_TS", ts); - } catch (Exception ex) { - System.setProperty("LOG_TS", String.valueOf(java.time.Instant.now().getEpochSecond())); - } - - // Create logs directory early to avoid races when Logback opens the file - try { - java.nio.file.Path logs = java.nio.file.Paths.get(System.getProperty("APP_HOME")).resolve("logs"); - java.nio.file.Files.createDirectories(logs); - } catch (Exception ex) { - // ignore - best effort - } - - // Delegate to the main application - com.studentgui.app.Main.main(args); - } -} +package com.studentgui.bootstrap; + +/** + * Lightweight bootstrapper that sets early system properties required by + * the logging subsystem (APP_HOME and LOG_TS) before delegating to the + * real application entry point. This ensures Logback picks up a stable + * per-run filename for the rolling file appender. + */ +public final class Bootstrap { + private Bootstrap() { throw new AssertionError("not instantiable"); } + + public static void main(final String[] args) { + try { + String appHome = com.studentgui.apphelpers.Helpers.APP_HOME.toString(); + System.setProperty("APP_HOME", appHome); + } catch (Throwable t) { + // Best-effort: if Helpers isn't available, fall back to a relative path + System.setProperty("APP_HOME", "app_home"); + } + // Ensure a stable per-run timestamp for Logback file naming. Use + // the same yyyyMMddHHmmss pattern that logback's + // element uses so filenames match when possible. + try { + java.time.format.DateTimeFormatter df = java.time.format.DateTimeFormatter.ofPattern("yyyyMMddHHmmss").withZone(java.time.ZoneOffset.UTC); + String ts = df.format(java.time.Instant.now()); + System.setProperty("LOG_TS", ts); + } catch (Exception ex) { + System.setProperty("LOG_TS", String.valueOf(java.time.Instant.now().getEpochSecond())); + } + + // Create logs directory early to avoid races when Logback opens the file + try { + java.nio.file.Path logs = java.nio.file.Paths.get(System.getProperty("APP_HOME")).resolve("logs"); + java.nio.file.Files.createDirectories(logs); + } catch (Exception ex) { + // ignore - best effort + } + + // Delegate to the main application + com.studentgui.app.Main.main(args); + } +} diff --git a/src/main/java/com/studentgui/test/BrailleSmokeTest.java b/src/main/java/com/studentgui/test/BrailleSmokeTest.java index 2087a82..26511f6 100644 --- a/src/main/java/com/studentgui/test/BrailleSmokeTest.java +++ b/src/main/java/com/studentgui/test/BrailleSmokeTest.java @@ -1,22 +1,22 @@ -package com.studentgui.test; - -/** - * Legacy smoke main retained for reference. Converted to a no-op deprecated - * holder to avoid duplicate Javadoc warnings now that an equivalent JUnit - * test exists under src/test/java. - * - * @deprecated Use {@code src/test/java/com/studentgui/test/BrailleSmokeTest.java} - * (the JUnit 5 replacement) for automated smoke testing. - */ -@Deprecated -public final class BrailleSmokeTest { - // intentionally empty - preserved for historical reference - - /** - * Private constructor to prevent instantiation of this utility holder. - * The real smoke test has been converted to a JUnit test under src/test. - */ - private BrailleSmokeTest() { - throw new AssertionError("Not instantiable"); - } -} +package com.studentgui.test; + +/** + * Legacy smoke main retained for reference. Converted to a no-op deprecated + * holder to avoid duplicate Javadoc warnings now that an equivalent JUnit + * test exists under src/test/java. + * + * @deprecated Use {@code src/test/java/com/studentgui/test/BrailleSmokeTest.java} + * (the JUnit 5 replacement) for automated smoke testing. + */ +@Deprecated +public final class BrailleSmokeTest { + // intentionally empty - preserved for historical reference + + /** + * Private constructor to prevent instantiation of this utility holder. + * The real smoke test has been converted to a JUnit test under src/test. + */ + private BrailleSmokeTest() { + throw new AssertionError("Not instantiable"); + } +} diff --git a/src/main/java/com/studentgui/tools/GroupedSmoke.java b/src/main/java/com/studentgui/tools/GroupedSmoke.java index d9e5fa5..2edef00 100644 --- a/src/main/java/com/studentgui/tools/GroupedSmoke.java +++ b/src/main/java/com/studentgui/tools/GroupedSmoke.java @@ -1,57 +1,97 @@ -package com.studentgui.tools; - -import java.nio.file.Path; -import java.time.LocalDate; -import java.time.format.DateTimeFormatter; -import java.util.ArrayList; -import java.util.List; - -import com.studentgui.apphelpers.Helpers; -import com.studentgui.apppages.JLineGraph; - -/** - * Small command-line helper that renders a sample grouped chart and - * writes an output PNG to the app data folder. Intended for smoke - * testing chart rendering during development and CI. - */ -public class GroupedSmoke { - /** - * Entry point for the grouped smoke utility. - * - * @param args ignored - * @throws Exception on unexpected IO or charting errors - */ - public static void main(final String[] args) throws Exception { - Helpers.createFolderHierarchy(); - JLineGraph graph = new JLineGraph(); - - // build part codes with P1_, P2_, P3_ groups (3+2+4 items) - String[] codes = new String[]{"P1_1","P1_2","P1_3","P2_1","P2_2","P3_1","P3_2","P3_3","P3_4"}; - - // Create sample data: 3 sessions - List> data = new ArrayList<>(); - for (int s = 0; s < 3; s++) { - List row = new ArrayList<>(); - for (int i = 0; i < codes.length; i++) { - row.add((i + s) % 5); - } - data.add(row); - } - graph.updateWithGroupedData(data, codes); - - Path outDir = Helpers.APP_HOME.resolve("StudentDataFiles").resolve(Helpers.safeName("Grouped Smoke")).resolve("plots"); - java.nio.file.Files.createDirectories(outDir); - DateTimeFormatter df = DateTimeFormatter.ISO_DATE; - Path outFile = outDir.resolve("GroupedSmoke-" + LocalDate.now().format(df) + ".png"); - graph.saveChart(outFile, 800, 600); - System.out.println("Grouped smoke wrote chart to: " + outFile.toAbsolutePath()); - System.out.println("Exists: " + java.nio.file.Files.exists(outFile)); - } - /** - * Public no-arg constructor to document the utility nature of this class. - * Kept for completeness; all work is performed from {@link #main(String[])}. - */ - public GroupedSmoke() { - // no state - } -} +package com.studentgui.tools; + +import java.nio.file.Path; +import java.time.LocalDate; +import java.time.format.DateTimeFormatter; +import java.util.ArrayList; +import java.util.List; + +import com.studentgui.apphelpers.Helpers; +import com.studentgui.apppages.JLineGraph; + +/** + * Automated smoke test for grouped chart rendering and multi-panel PNG export. + * + *

Verifies that {@link JLineGraph} correctly renders multiple stacked phase-grouped + * charts (as used by assessment pages like Braille, Abacus, etc.). Generates synthetic + * data with explicit phase prefixes (P1, P2, P3) and exports to PNG.

+ * + *

Purpose:

+ *
    + *
  • Validates phase grouping logic in {@link JLineGraph#updateWithGroupedData}
  • + *
  • Ensures each group renders as a separate stacked chart panel
  • + *
  • Verifies PNG export of multi-chart layouts
  • + *
  • Provides visual reference for chart appearance during development
  • + *
+ * + *

Usage:

+ *
{@code
+ * java -cp StudentDataGUI.jar com.studentgui.tools.GroupedSmoke
+ * }
+ * + *

Expected Output:

+ *
+ * Grouped smoke wrote chart to: /path/to/app_home/StudentDataFiles/Grouped_Smoke/plots/GroupedSmoke-2024-01-15.png
+ * Exists: true
+ * 
+ * + *

Test Data Structure:

+ *
    + *
  • Part codes: 9 codes with prefixes: P1 (3 items), P2 (2 items), P3 (4 items)
  • + *
  • Sessions: 3 synthetic sessions with deterministic scores {@code (i + s) % 5}
  • + *
  • Expected output: 3 stacked chart panels (one per phase group) in a single 800×600px PNG
  • + *
+ * + *

Output Location: {@code app_home/StudentDataFiles/Grouped_Smoke/plots/GroupedSmoke-.png}

+ * + *

Validation: Inspect the generated PNG to verify:

+ *
    + *
  1. Three distinct chart panels labeled "P1 - 3 items", "P2 - 2 items", "P3 - 4 items"
  2. + *
  3. Each panel shows 3 line series (2 gray historical, 1 black latest)
  4. + *
  5. Colored background bands visible in all panels
  6. + *
+ * + * @see com.studentgui.apppages.JLineGraph#updateWithGroupedData + * @see com.studentgui.apppages.JLineGraph#saveChart + */ +public class GroupedSmoke { + /** + * Entry point for the grouped smoke utility. + * + * @param args ignored + * @throws Exception on unexpected IO or charting errors + */ + public static void main(final String[] args) throws Exception { + Helpers.createFolderHierarchy(); + JLineGraph graph = new JLineGraph(); + + // build part codes with P1_, P2_, P3_ groups (3+2+4 items) + String[] codes = new String[]{"P1_1","P1_2","P1_3","P2_1","P2_2","P3_1","P3_2","P3_3","P3_4"}; + + // Create sample data: 3 sessions + List> data = new ArrayList<>(); + for (int s = 0; s < 3; s++) { + List row = new ArrayList<>(); + for (int i = 0; i < codes.length; i++) { + row.add((i + s) % 5); + } + data.add(row); + } + graph.updateWithGroupedData(data, codes); + + Path outDir = Helpers.APP_HOME.resolve("StudentDataFiles").resolve(Helpers.safeName("Grouped Smoke")).resolve("plots"); + java.nio.file.Files.createDirectories(outDir); + DateTimeFormatter df = DateTimeFormatter.ISO_DATE; + Path outFile = outDir.resolve("GroupedSmoke-" + LocalDate.now().format(df) + ".png"); + graph.saveChart(outFile, 800, 600); + System.out.println("Grouped smoke wrote chart to: " + outFile.toAbsolutePath()); + System.out.println("Exists: " + java.nio.file.Files.exists(outFile)); + } + /** + * Public no-arg constructor to document the utility nature of this class. + * Kept for completeness; all work is performed from {@link #main(String[])}. + */ + public GroupedSmoke() { + // no state + } +} diff --git a/src/main/java/com/studentgui/tools/ProgrammaticPageSaveTest.java b/src/main/java/com/studentgui/tools/ProgrammaticPageSaveTest.java index 81f111e..0ffbf3f 100644 --- a/src/main/java/com/studentgui/tools/ProgrammaticPageSaveTest.java +++ b/src/main/java/com/studentgui/tools/ProgrammaticPageSaveTest.java @@ -1,92 +1,133 @@ -package com.studentgui.tools; - -import java.time.LocalDate; -import java.util.Arrays; -import java.util.stream.IntStream; - -import javax.swing.JButton; - -import com.studentgui.apphelpers.Helpers; -import com.studentgui.apppages.Braille; -import com.studentgui.apppages.JLineGraph; - -/** - * Programmatically create a Braille page, populate PhaseScoreField values, - * and trigger the submit action to verify DB insert and PNG export. - *

- * This small test runs without user interaction and is useful during - * automated smoke tests or developer verification of page submission - * behaviour. Outputs are written under the application's app_home. - *

- */ -public class ProgrammaticPageSaveTest { - /** - * Program entry to run the programmatic page save test. - * - * @param args ignored - * @throws Exception on reflection or DB errors - */ - public static void main(final String[] args) throws Exception { - Helpers.createFolderHierarchy(); - JLineGraph graph = new JLineGraph(); - Braille page = new Braille("Smoke Test", LocalDate.now(), graph); - - // Set all fields to 3 via getComponents traversal - Arrays.stream(page.getComponents()).forEach(c -> { - // nothing here; we'll rely on the submit button to collect values from the internal PhaseScoreField instances - }); - - // Helper: find submit button by accessible name and click it - JButton submit = findButtonByAccessibleName(page, "Submit Braille Data"); - if (submit == null) { - System.out.println("Submit button not found; aborting test"); - return; - } - - // Programmatically set values using the page's declared skillFields via reflection - try { - java.lang.reflect.Field f = Braille.class.getDeclaredField("skillFields"); - f.setAccessible(true); - Object arr = f.get(page); - if (arr instanceof com.studentgui.uicomp.PhaseScoreField[]) { - com.studentgui.uicomp.PhaseScoreField[] s = (com.studentgui.uicomp.PhaseScoreField[]) arr; - IntStream.range(0, s.length).forEach(i -> s[i].setValue(3)); - } - } catch (ReflectiveOperationException roe) { - roe.printStackTrace(); - System.out.println("Unable to set skillFields via reflection"); - } - - // Trigger submit - System.out.println("Triggering submit button action..."); - submit.doClick(); - - System.out.println("Programmatic submit triggered. Check app_home for outputs."); - } - - private static JButton findButtonByAccessibleName(final java.awt.Container c, final String name) { - for (java.awt.Component comp : c.getComponents()) { - if (comp instanceof JButton) { - JButton b = (JButton) comp; - if (name.equals(b.getAccessibleContext().getAccessibleName())) { - return b; - } - } - if (comp instanceof java.awt.Container) { - JButton r = findButtonByAccessibleName((java.awt.Container) comp, name); - if (r != null) { - return r; - } - } - } - return null; - } - - /** - * Private constructor to avoid instantiation - this class is a programmatic - * test harness containing only static helpers and a main method. - */ - private ProgrammaticPageSaveTest() { - // no instances - } -} +package com.studentgui.tools; + +import java.time.LocalDate; +import java.util.Arrays; +import java.util.stream.IntStream; + +import javax.swing.JButton; + +import com.studentgui.apphelpers.Helpers; +import com.studentgui.apppages.Braille; +import com.studentgui.apppages.JLineGraph; + +/** + * Automated integration test for programmatic page manipulation and database submission. + * + *

Simulates user interaction with the {@link Braille} assessment page by:

+ *
    + *
  1. Programmatically instantiating a Braille page with synthetic student/date
  2. + *
  3. Using reflection to access and populate internal {@code PhaseScoreField} components
  4. + *
  5. Locating the "Submit Braille Data" button via accessible name
  6. + *
  7. Programmatically triggering the submit action via {@link JButton#doClick()}
  8. + *
+ * + *

Purpose:

+ *
    + *
  • Validates end-to-end page submission workflow without GUI interaction
  • + *
  • Tests database insert, JSON export, and PNG chart generation in automated context
  • + *
  • Verifies reflection-based access to page internals for testing purposes
  • + *
  • Provides reference for programmatic testing of other assessment pages
  • + *
+ * + *

Usage:

+ *
{@code
+ * java -cp StudentDataGUI.jar com.studentgui.tools.ProgrammaticPageSaveTest
+ * }
+ * + *

Expected Side Effects:

+ *
    + *
  • New Braille progress session inserted into database for student "Smoke Test"
  • + *
  • JSON export written to {@code StudentDataFiles/Smoke_Test/Sessions/Braille/}
  • + *
  • Phase-grouped PNG plots written to {@code StudentDataFiles/Smoke_Test/plots/}
  • + *
  • Markdown and HTML reports generated in {@code StudentDataFiles/Smoke_Test/reports/}
  • + *
+ * + *

Reflection Usage: Accesses private {@code skillFields} array in {@link Braille} + * to set all 64 Braille skills to a score of 3. This demonstrates how to programmatically + * manipulate page state for testing when public setters are not available.

+ * + *

Validation: After execution, inspect:

+ *
    + *
  • Database: {@code sqlite3 app_home/StudentDatabase/students.db "SELECT * FROM ProgressSession ORDER BY id DESC LIMIT 1;"}
  • + *
  • JSON exports: {@code ls -lt app_home/StudentDataFiles/Smoke_Test/Sessions/Braille/}
  • + *
  • Generated plots: {@code ls -lt app_home/StudentDataFiles/Smoke_Test/plots/}
  • + *
+ * + *

Note: This test modifies the live database. Run in a test environment or + * use a separate APP_HOME directory to avoid polluting production data.

+ * + * @see com.studentgui.apppages.Braille + * @see com.studentgui.uicomp.PhaseScoreField + * @see javax.swing.JButton#doClick() + */ +public class ProgrammaticPageSaveTest { + /** + * Program entry to run the programmatic page save test. + * + * @param args ignored + * @throws Exception on reflection or DB errors + */ + public static void main(final String[] args) throws Exception { + Helpers.createFolderHierarchy(); + JLineGraph graph = new JLineGraph(); + Braille page = new Braille("Smoke Test", LocalDate.now(), graph); + + // Set all fields to 3 via getComponents traversal + Arrays.stream(page.getComponents()).forEach(c -> { + // nothing here; we'll rely on the submit button to collect values from the internal PhaseScoreField instances + }); + + // Helper: find submit button by accessible name and click it + JButton submit = findButtonByAccessibleName(page, "Submit Braille Data"); + if (submit == null) { + System.out.println("Submit button not found; aborting test"); + return; + } + + // Programmatically set values using the page's declared skillFields via reflection + try { + java.lang.reflect.Field f = Braille.class.getDeclaredField("skillFields"); + f.setAccessible(true); + Object arr = f.get(page); + if (arr instanceof com.studentgui.uicomp.PhaseScoreField[]) { + com.studentgui.uicomp.PhaseScoreField[] s = (com.studentgui.uicomp.PhaseScoreField[]) arr; + IntStream.range(0, s.length).forEach(i -> s[i].setValue(3)); + } + } catch (ReflectiveOperationException roe) { + roe.printStackTrace(); + System.out.println("Unable to set skillFields via reflection"); + } + + // Trigger submit + System.out.println("Triggering submit button action..."); + submit.doClick(); + + System.out.println("Programmatic submit triggered. Check app_home for outputs."); + } + + private static JButton findButtonByAccessibleName(final java.awt.Container c, final String name) { + for (java.awt.Component comp : c.getComponents()) { + if (comp instanceof JButton) { + JButton b = (JButton) comp; + if (name.equals(b.getAccessibleContext().getAccessibleName())) { + return b; + } + } + if (comp instanceof java.awt.Container) { + JButton r = findButtonByAccessibleName((java.awt.Container) comp, name); + if (r != null) { + return r; + } + } + } + return null; + } + + /** + * Private constructor to avoid instantiation - this class is a programmatic + * test harness containing only static helpers and a main method. + */ + private ProgrammaticPageSaveTest() { + // no instances + } +} diff --git a/src/main/java/com/studentgui/tools/QueryStudentData.java b/src/main/java/com/studentgui/tools/QueryStudentData.java index 41eec7c..64df051 100644 --- a/src/main/java/com/studentgui/tools/QueryStudentData.java +++ b/src/main/java/com/studentgui/tools/QueryStudentData.java @@ -1,78 +1,115 @@ -package com.studentgui.tools; - -import java.sql.Connection; -import java.sql.DriverManager; -import java.sql.PreparedStatement; -import java.sql.ResultSet; -import java.util.List; - -import com.studentgui.apphelpers.Database; -import com.studentgui.apphelpers.Helpers; - -/** - * Development utility to inspect available students and their recent - * progress session rows. Prints basic statistics to stdout and is - * intended for debugging or manual data inspection. - */ -public class QueryStudentData { - /** - * Command-line entry point. Prints progress types and a sample row for - * the specified or first-known student. - * - * @param args optional first argument is student display name - * @throws Exception on database errors - */ - public static void main(final String[] args) throws Exception { - Helpers.createFolderHierarchy(); - List students = Helpers.getStudents(); - String student = null; - if (args.length > 0) { - student = args[0]; - } - if (student == null) { - System.out.println("Known students:"); - for (String s : students) { - System.out.println(" - " + s); - } - if (!students.isEmpty()) { - student = students.get(0); - } else { - System.out.println("No students found in DB. Exiting."); - return; - } - } - System.out.println("Inspecting student: " + student); - // list progress types - try (Connection c = DriverManager.getConnection("jdbc:sqlite:" + Helpers.DATABASE_PATH.toString())) { - try (PreparedStatement ps = c.prepareStatement("SELECT id, name FROM ProgressType")) { - try (ResultSet rs = ps.executeQuery()) { - while (rs.next()) { - int ptId = rs.getInt("id"); - String ptName = rs.getString("name"); - // count parts - int partCount = 0; - try (PreparedStatement ps2 = c.prepareStatement("SELECT COUNT(*) FROM AssessmentPart WHERE progress_type_id = ?")) { - ps2.setInt(1, ptId); - try (ResultSet rs2 = ps2.executeQuery()) { - if (rs2.next()) { - partCount = rs2.getInt(1); - } - } - } - List> rows = Database.fetchLatestAssessmentResults(student, ptName, 5); - System.out.println(String.format("ProgressType '%s' (id=%d) parts=%d sessions=%d", ptName, ptId, partCount, rows.size())); - if (!rows.isEmpty()) { - System.out.println(" Sample row sizes: " + rows.get(0).size() + " values: " + rows.get(0)); - } - } - } - } - } - } - /** - * No-op public constructor to document this class as a small utility. - */ - public QueryStudentData() { - // utility class; no state - } -} +package com.studentgui.tools; + +import java.sql.Connection; +import java.sql.DriverManager; +import java.sql.PreparedStatement; +import java.sql.ResultSet; +import java.util.List; + +import com.studentgui.apphelpers.Database; +import com.studentgui.apphelpers.Helpers; + +/** + * Command-line inspection tool for viewing student database contents and schema statistics. + * + *

Provides a quick diagnostic view of database state without launching the GUI. + * Useful for:

+ *
    + *
  • Verifying student records exist in the database
  • + *
  • Inspecting available progress types and their assessment part counts
  • + *
  • Checking session data row sizes for debugging schema migrations
  • + *
  • Quick manual data verification during development or troubleshooting
  • + *
+ * + *

Usage:

+ *
{@code
+ * # List all students and progress types with counts
+ * java -cp StudentDataGUI.jar com.studentgui.tools.QueryStudentData
+ *
+ * # Inspect specific student's progress types
+ * java -cp StudentDataGUI.jar com.studentgui.tools.QueryStudentData "Aaron A Aaronsson"
+ * }
+ * + *

Output Format:

+ *
+ * Inspecting student: Aaron A Aaronsson
+ * ProgressType 'Braille' (id=1) parts=64 sessions=3
+ *  Sample row sizes: 64 values: [2, 3, 2, 3, 4, ...]
+ * ProgressType 'Abacus' (id=2) parts=22 sessions=1
+ *  Sample row sizes: 22 values: [0, 1, 2, 1, 3, ...]
+ * 
+ * + *

Workflow:

+ *
    + *
  1. Lists all known students via {@link Helpers#getStudents()}
  2. + *
  3. Selects first student or uses command-line argument
  4. + *
  5. Queries {@code ProgressType} table for all progress types
  6. + *
  7. For each progress type: counts assessment parts and fetches sample session rows
  8. + *
  9. Prints progress type name, ID, part count, session count, and sample row to stdout
  10. + *
+ * + * @see com.studentgui.apphelpers.Database#fetchLatestAssessmentResults + * @see com.studentgui.apphelpers.Helpers#getStudents() + */ +public class QueryStudentData { + /** + * Command-line entry point. Prints progress types and a sample row for + * the specified or first-known student. + * + * @param args optional first argument is student display name + * @throws Exception on database errors + */ + public static void main(final String[] args) throws Exception { + Helpers.createFolderHierarchy(); + List students = Helpers.getStudents(); + String student = null; + if (args.length > 0) { + student = args[0]; + } + if (student == null) { + System.out.println("Known students:"); + for (String s : students) { + System.out.println(" - " + s); + } + if (!students.isEmpty()) { + student = students.get(0); + } else { + System.out.println("No students found in DB. Exiting."); + return; + } + } + System.out.println("Inspecting student: " + student); + // list progress types + try (Connection c = DriverManager.getConnection("jdbc:sqlite:" + Helpers.DATABASE_PATH.toString())) { + try (PreparedStatement ps = c.prepareStatement("SELECT id, name FROM ProgressType")) { + try (ResultSet rs = ps.executeQuery()) { + while (rs.next()) { + int ptId = rs.getInt("id"); + String ptName = rs.getString("name"); + // count parts + int partCount = 0; + try (PreparedStatement ps2 = c.prepareStatement("SELECT COUNT(*) FROM AssessmentPart WHERE progress_type_id = ?")) { + ps2.setInt(1, ptId); + try (ResultSet rs2 = ps2.executeQuery()) { + if (rs2.next()) { + partCount = rs2.getInt(1); + } + } + } + List> rows = Database.fetchLatestAssessmentResults(student, ptName, 5); + System.out.println(String.format("ProgressType '%s' (id=%d) parts=%d sessions=%d", ptName, ptId, partCount, rows.size())); + if (!rows.isEmpty()) { + System.out.println(" Sample row sizes: " + rows.get(0).size() + " values: " + rows.get(0)); + } + } + } + } + } + } + /** + * No-op public constructor to document this class as a small utility. + */ + public QueryStudentData() { + // utility class; no state + } +} diff --git a/src/main/java/com/studentgui/tools/RenderStudentProgress.java b/src/main/java/com/studentgui/tools/RenderStudentProgress.java index 2534cc1..fb8b61f 100644 --- a/src/main/java/com/studentgui/tools/RenderStudentProgress.java +++ b/src/main/java/com/studentgui/tools/RenderStudentProgress.java @@ -1,74 +1,102 @@ -package com.studentgui.tools; - -import java.nio.file.Path; -import java.sql.Connection; -import java.sql.DriverManager; -import java.sql.PreparedStatement; -import java.sql.ResultSet; -import java.time.LocalDate; -import java.time.format.DateTimeFormatter; -import java.util.ArrayList; -import java.util.List; - -import com.studentgui.apphelpers.Database; -import com.studentgui.apphelpers.Helpers; -import com.studentgui.apppages.JLineGraph; - -/** - * Command-line tool to render a particular student's progress chart - * for a named progress type. Produces a PNG in the student's plots - * directory. Useful for offline rendering and debugging chart output. - */ -public class RenderStudentProgress { - /** - * Render and write a progress chart for the provided student and progress type. - * - * @param args first arg: student display name, second arg: progress type name - * @throws Exception on I/O or database access errors - */ - public static void main(final String[] args) throws Exception { - if (args.length < 2) { - System.out.println("Usage: RenderStudentProgress "); - return; - } - String student = args[0]; - String pt = args[1]; - Helpers.createFolderHierarchy(); - System.out.println("Rendering " + pt + " for " + student); - - // fetch canonical part codes for progress type - List codes = new ArrayList<>(); - try (Connection c = DriverManager.getConnection("jdbc:sqlite:" + Helpers.DATABASE_PATH.toString())) { - try (PreparedStatement ps = c.prepareStatement("SELECT code FROM AssessmentPart ap JOIN ProgressType pt ON ap.progress_type_id = pt.id WHERE pt.name = ? ORDER BY ap.id ASC")) { - ps.setString(1, pt); - try (ResultSet rs = ps.executeQuery()) { - while (rs.next()) codes.add(rs.getString(1)); - } - } - } - if (codes.isEmpty()) { - System.out.println("No parts found for progress type: " + pt); - return; - } - String[] codeArr = codes.toArray(new String[0]); - List> rows = Database.fetchLatestAssessmentResults(student, pt, 5); - if (rows == null || rows.isEmpty()) { - System.out.println("No session rows for student/progress: " + student + "/" + pt); - return; - } - JLineGraph g = new JLineGraph(); - g.updateWithGroupedData(rows, codeArr); - Path out = Helpers.APP_HOME.resolve("StudentDataFiles").resolve(Helpers.safeName(student)).resolve("plots"); - java.nio.file.Files.createDirectories(out); - DateTimeFormatter df = DateTimeFormatter.ISO_DATE; - Path file = out.resolve(pt + "-render-" + LocalDate.now().format(df) + ".png"); - g.saveChart(file, 1000, 800); - System.out.println("Wrote: " + file.toAbsolutePath()); - } - /** - * Explicit no-arg constructor with documentation to avoid default-constructor javadoc warnings. - */ - public RenderStudentProgress() { - // utility - } -} +package com.studentgui.tools; + +import java.nio.file.Path; +import java.sql.Connection; +import java.sql.DriverManager; +import java.sql.PreparedStatement; +import java.sql.ResultSet; +import java.time.LocalDate; +import java.time.format.DateTimeFormatter; +import java.util.ArrayList; +import java.util.List; + +import com.studentgui.apphelpers.Database; +import com.studentgui.apphelpers.Helpers; +import com.studentgui.apppages.JLineGraph; + +/** + * Command-line utility for offline student progress chart rendering and export. + * + *

This standalone tool generates PNG charts for a specific student and progress type + * without launching the full GUI application. Useful for:

+ *
    + *
  • Batch chart generation for multiple students/progress types
  • + *
  • Debugging chart rendering issues outside the GUI context
  • + *
  • Automated report generation in CI/CD pipelines
  • + *
  • Creating historical chart snapshots for archival purposes
  • + *
+ * + *

Usage:

+ *
{@code
+ * java -cp StudentDataGUI.jar com.studentgui.tools.RenderStudentProgress "Aaron A Aaronsson" "Braille"
+ * }
+ * + *

Workflow:

+ *
    + *
  1. Ensures app folder hierarchy exists via {@link Helpers#createFolderHierarchy()}
  2. + *
  3. Queries database for canonical assessment part codes for the specified progress type
  4. + *
  5. Fetches up to 5 most recent assessment sessions via {@link Database#fetchLatestAssessmentResults}
  6. + *
  7. Renders grouped chart using {@link JLineGraph#updateWithGroupedData}
  8. + *
  9. Exports PNG to {@code StudentDataFiles//plots/-render-.png}
  10. + *
+ * + *

Output: PNG file written to student's plots directory with filename format: + * {@code -render-.png}

+ * + * @see com.studentgui.apphelpers.Database#fetchLatestAssessmentResults + * @see com.studentgui.apppages.JLineGraph + * @see com.studentgui.apphelpers.Helpers#createFolderHierarchy() + */ +public class RenderStudentProgress { + /** + * Render and write a progress chart for the provided student and progress type. + * + * @param args first arg: student display name, second arg: progress type name + * @throws Exception on I/O or database access errors + */ + public static void main(final String[] args) throws Exception { + if (args.length < 2) { + System.out.println("Usage: RenderStudentProgress "); + return; + } + String student = args[0]; + String pt = args[1]; + Helpers.createFolderHierarchy(); + System.out.println("Rendering " + pt + " for " + student); + + // fetch canonical part codes for progress type + List codes = new ArrayList<>(); + try (Connection c = DriverManager.getConnection("jdbc:sqlite:" + Helpers.DATABASE_PATH.toString())) { + try (PreparedStatement ps = c.prepareStatement("SELECT code FROM AssessmentPart ap JOIN ProgressType pt ON ap.progress_type_id = pt.id WHERE pt.name = ? ORDER BY ap.id ASC")) { + ps.setString(1, pt); + try (ResultSet rs = ps.executeQuery()) { + while (rs.next()) codes.add(rs.getString(1)); + } + } + } + if (codes.isEmpty()) { + System.out.println("No parts found for progress type: " + pt); + return; + } + String[] codeArr = codes.toArray(new String[0]); + List> rows = Database.fetchLatestAssessmentResults(student, pt, 5); + if (rows == null || rows.isEmpty()) { + System.out.println("No session rows for student/progress: " + student + "/" + pt); + return; + } + JLineGraph g = new JLineGraph(); + g.updateWithGroupedData(rows, codeArr); + Path out = Helpers.APP_HOME.resolve("StudentDataFiles").resolve(Helpers.safeName(student)).resolve("plots"); + java.nio.file.Files.createDirectories(out); + DateTimeFormatter df = DateTimeFormatter.ISO_DATE; + Path file = out.resolve(pt + "-render-" + LocalDate.now().format(df) + ".png"); + g.saveChart(file, 1000, 800); + System.out.println("Wrote: " + file.toAbsolutePath()); + } + /** + * Explicit no-arg constructor with documentation to avoid default-constructor javadoc warnings. + */ + public RenderStudentProgress() { + // utility + } +} diff --git a/src/main/java/com/studentgui/tools/SmokeTest.java b/src/main/java/com/studentgui/tools/SmokeTest.java index 8a01f50..dd2d5a1 100644 --- a/src/main/java/com/studentgui/tools/SmokeTest.java +++ b/src/main/java/com/studentgui/tools/SmokeTest.java @@ -1,55 +1,85 @@ -package com.studentgui.tools; - -import java.nio.file.Path; -import java.time.LocalDate; -import java.time.format.DateTimeFormatter; -import java.util.ArrayList; -import java.util.List; - -import com.studentgui.apphelpers.Helpers; -import com.studentgui.apppages.JLineGraph; - -/** - * Minimal smoke test to exercise the chart rendering and file export. - *

- * Generates deterministic sample data, renders it via {@code JLineGraph} - * and writes a PNG under the app_home plots directory. - *

- */ -public class SmokeTest { - /** - * Entry point for the smoke test. - * - * @param args ignored - * @throws Exception on IO or chart errors - */ - public static void main(final String[] args) throws Exception { - Helpers.createFolderHierarchy(); - JLineGraph graph = new JLineGraph(); - - // Create sample data: 3 sessions, each with 28 skill values (0-4) - List> data = new ArrayList<>(); - for (int s = 0; s < 3; s++) { - List row = new ArrayList<>(); - for (int i = 0; i < 28; i++) { - row.add((i + s) % 5); // deterministic sample - } - data.add(row); - } - graph.updateWithData(data); - - Path outDir = Helpers.APP_HOME.resolve("StudentDataFiles").resolve(Helpers.safeName("Smoke Test")).resolve("plots"); - DateTimeFormatter df = DateTimeFormatter.ISO_DATE; - Path outFile = outDir.resolve("SmokeTest-" + LocalDate.now().format(df) + ".png"); - graph.saveChart(outFile, 800, 400); - System.out.println("Smoke test wrote chart to: " + outFile.toAbsolutePath()); - System.out.println("Exists: " + java.nio.file.Files.exists(outFile)); - } - - /** - * Private constructor to prevent instantiation of this utility test class. - */ - private SmokeTest() { - // no instances - } -} +package com.studentgui.tools; + +import java.nio.file.Path; +import java.time.LocalDate; +import java.time.format.DateTimeFormatter; +import java.util.ArrayList; +import java.util.List; + +import com.studentgui.apphelpers.Helpers; +import com.studentgui.apppages.JLineGraph; + +/** + * Minimal automated smoke test for chart rendering and PNG export functionality. + * + *

Generates deterministic synthetic assessment data, renders it via {@link JLineGraph}, + * and writes a PNG to the app data folder. Used to verify:

+ *
    + *
  • JFreeChart rendering pipeline functions correctly
  • + *
  • PNG export via {@link JLineGraph#saveChart} produces valid image files
  • + *
  • File I/O permissions and folder creation work as expected
  • + *
  • Chart layout and visual appearance match expectations (manual review)
  • + *
+ * + *

Usage:

+ *
{@code
+ * java -cp StudentDataGUI.jar com.studentgui.tools.SmokeTest
+ * }
+ * + *

Expected Output:

+ *
+ * Smoke test wrote chart to: /path/to/app_home/StudentDataFiles/Smoke_Test/plots/SmokeTest-2024-01-15.png
+ * Exists: true
+ * 
+ * + *

Test Data: Generates 3 synthetic sessions with 28 skills each, using + * the formula {@code (skillIndex + sessionIndex) % 5} to produce deterministic + * values in the 0–4 range.

+ * + *

Output Location: {@code app_home/StudentDataFiles/Smoke_Test/plots/SmokeTest-.png}

+ * + *

Validation: Success is indicated by "Exists: true" output and a valid + * 800×400px PNG file at the reported path. Visual inspection of the chart should show + * 3 line series (2 gray, 1 black) with colored background bands.

+ * + * @see com.studentgui.apppages.JLineGraph#updateWithData + * @see com.studentgui.apppages.JLineGraph#saveChart + * @see com.studentgui.apphelpers.Helpers#createFolderHierarchy() + */ +public class SmokeTest { + /** + * Entry point for the smoke test. + * + * @param args ignored + * @throws Exception on IO or chart errors + */ + public static void main(final String[] args) throws Exception { + Helpers.createFolderHierarchy(); + JLineGraph graph = new JLineGraph(); + + // Create sample data: 3 sessions, each with 28 skill values (0-4) + List> data = new ArrayList<>(); + for (int s = 0; s < 3; s++) { + List row = new ArrayList<>(); + for (int i = 0; i < 28; i++) { + row.add((i + s) % 5); // deterministic sample + } + data.add(row); + } + graph.updateWithData(data); + + Path outDir = Helpers.APP_HOME.resolve("StudentDataFiles").resolve(Helpers.safeName("Smoke Test")).resolve("plots"); + DateTimeFormatter df = DateTimeFormatter.ISO_DATE; + Path outFile = outDir.resolve("SmokeTest-" + LocalDate.now().format(df) + ".png"); + graph.saveChart(outFile, 800, 400); + System.out.println("Smoke test wrote chart to: " + outFile.toAbsolutePath()); + System.out.println("Exists: " + java.nio.file.Files.exists(outFile)); + } + + /** + * Private constructor to prevent instantiation of this utility test class. + */ + private SmokeTest() { + // no instances + } +} diff --git a/src/main/java/com/studentgui/uicomp/PhaseScoreField.java b/src/main/java/com/studentgui/uicomp/PhaseScoreField.java index 6b8ecad..291c2af 100644 --- a/src/main/java/com/studentgui/uicomp/PhaseScoreField.java +++ b/src/main/java/com/studentgui/uicomp/PhaseScoreField.java @@ -1,271 +1,271 @@ -package com.studentgui.uicomp; - -import java.awt.Dimension; -import java.awt.Font; -import java.awt.GridBagConstraints; -import java.awt.GridBagLayout; -import java.awt.Insets; - -import javax.swing.BorderFactory; -import javax.swing.JComponent; -import javax.swing.JPanel; -import javax.swing.JSpinner; -import javax.swing.JTextArea; -import javax.swing.SpinnerNumberModel; - -/** - * Reusable component that renders a wrapped descriptive label and a compact - * integer input (0..4). The label is a non-editable JTextArea that wraps at - * ~200px; the component adds a 20px left inset so the label appears offset. - * The spinner is aligned to the first line of the label. - */ -public class PhaseScoreField extends JPanel { - private static final long serialVersionUID = 1L; - private static final org.slf4j.Logger LOG = org.slf4j.LoggerFactory.getLogger(PhaseScoreField.class); - // Ensure we only log the font-adjustment debug once to avoid noisy output in headless tests. - private static final java.util.concurrent.atomic.AtomicBoolean FONT_ADJUST_LOGGED = new java.util.concurrent.atomic.AtomicBoolean(false); - /** Wrapped, read-only label area used to display the description text. */ - private final JTextArea labelArea; - /** Numeric spinner used for 0..4 score entry. */ - private final JSpinner spinner; - /** Container used to constrain the wrap width of the label area. */ - private final JPanel labelWrap; - // Global label width (pixels) used to make all rows align; default ~200 - // Note: intentionally non-final so pages can adjust it at runtime. - private static int globalLabelWidthPx = 200; - - /** Horizontal spacer panel inserted to tune the gap between label and spinner. */ - private final JPanel spacer; - - /** - * Create a PhaseScoreField containing a wrapped label and a numeric spinner. - * - * @param labelText text label to display (may be multi-line) - * @param initial initial integer value for the spinner (0..4) - */ - public PhaseScoreField(final String labelText, final int initial) { - super(new GridBagLayout()); - this.labelArea = new JTextArea(labelText); - labelArea.setLineWrap(true); - labelArea.setWrapStyleWord(true); - labelArea.setEditable(false); - labelArea.setOpaque(false); - labelArea.setFocusable(false); - // Use explicit font so the appearance doesn't change when switching LAFs - Font labelFont = new Font(Font.SANS_SERIF, Font.PLAIN, 12); - labelArea.setFont(labelFont); - // Constrain width to the configured global label width so it doesn't expand. - // Pages set GLOBAL_LABEL_WIDTH_PX to (maxLabelPx + 50). We render the label - // area at (GLOBAL - 50) and insert a 50px spacer so the spinner sits - // exactly 50px after the longest label text. - int prefHeight = computePreferredHeight(labelFont, 2); - int labelWidth = Math.max(40, globalLabelWidthPx - 50); - java.awt.Dimension fixed = new java.awt.Dimension(labelWidth, prefHeight); - // Wrap the JTextArea in a small container to guarantee horizontal size - this.labelWrap = new JPanel(new java.awt.BorderLayout()); - this.labelWrap.setPreferredSize(fixed); - this.labelWrap.setMinimumSize(fixed); - this.labelWrap.setMaximumSize(new java.awt.Dimension(labelWidth, Short.MAX_VALUE)); - this.labelWrap.add(labelArea, java.awt.BorderLayout.CENTER); - - this.spinner = new JSpinner(new SpinnerNumberModel(initial, 0, 4, 1)); - JComponent editor = spinner.getEditor(); - // Set explicit font for spinner editor to keep sizing consistent across themes - Font spinnerFont = new Font(Font.SANS_SERIF, Font.PLAIN, 12); - editor.setFont(spinnerFont); - // The editor is typically a JSpinner.DefaultEditor containing a JTextField - try { - java.lang.reflect.Field f = editor.getClass().getDeclaredField("textField"); - f.setAccessible(true); - Object tf = f.get(editor); - if (tf instanceof javax.swing.JTextField tfField) { - tfField.setFont(spinnerFont); - } - } catch (ReflectiveOperationException roe) { - // Field may not exist on some LAF/editor implementations. Log this at most once - // to avoid excessive noise during automated test runs. - if (FONT_ADJUST_LOGGED.compareAndSet(false, true)) { - LOG.trace("Could not adjust spinner editor textField font (field missing or inaccessible)"); - } - } - editor.setPreferredSize(new Dimension(48, 20)); - spinner.setPreferredSize(new Dimension(48, 24)); - - setBorder(BorderFactory.createEmptyBorder(0, 20, 0, 0)); // left inset 20px - - GridBagConstraints gbc = new GridBagConstraints(); - // Label: fixed preferred width, do not expand horizontally - gbc.gridx = 0; - gbc.gridy = 0; - gbc.anchor = GridBagConstraints.NORTHWEST; - gbc.fill = GridBagConstraints.NONE; // keep label at preferred size - gbc.weightx = 0.0; - gbc.insets = new Insets(2, 2, 2, 8); - add(labelWrap, gbc); - - // Spacer: compute width so the spinner ends up 50px after the rendered label text - gbc.gridx = 1; - gbc.gridy = 0; - gbc.anchor = GridBagConstraints.NORTHWEST; - gbc.fill = GridBagConstraints.NONE; - gbc.weightx = 0.0; - // Compute rendered text pixel width for this label (safe to call here) - int textPx = computeMaxLabelPixelWidth(labelFont, new String[] { labelText }); - int paddingWithinWrap = Math.max(0, labelWidth - textPx); - int spacerWidth = Math.max(0, 50 - paddingWithinWrap); - this.spacer = new JPanel(); this.spacer.setPreferredSize(new java.awt.Dimension(spacerWidth, 1)); - add(this.spacer, gbc); - - // Spinner sits immediately to the right of the spacer - gbc.gridx = 2; - gbc.gridy = 0; - gbc.anchor = GridBagConstraints.NORTHWEST; - gbc.fill = GridBagConstraints.NONE; - gbc.weightx = 0.0; - add(spinner, gbc); - - // Filler: consumes remaining horizontal space so the spinner doesn't get pushed to the far right - gbc.gridx = 3; - gbc.gridy = 0; - gbc.anchor = GridBagConstraints.NORTHWEST; - gbc.fill = GridBagConstraints.HORIZONTAL; - gbc.weightx = 1.0; - add(new JPanel(), gbc); - - // After layout, adjust spacer so the visible gap between label and spinner is exactly 50px - javax.swing.SwingUtilities.invokeLater(() -> { - int labelRight = labelWrap.getX() + labelWrap.getWidth(); - int actualGap = spinner.getX() - labelRight; - int desiredGap = 50; - int currentSpacer = this.spacer.getPreferredSize().width; - int delta = desiredGap - actualGap; - if (delta != 0) { - int newWidth = Math.max(0, currentSpacer + delta); - this.spacer.setPreferredSize(new java.awt.Dimension(newWidth, 1)); - this.spacer.revalidate(); - this.revalidate(); - this.repaint(); - } - }); - } - - /** - * Set a global label width used by all PhaseScoreField instances created - * after calling this method. This helps align the spinner input across - * multiple rows so the entry fields start at a consistent position. - * - * @param px target global label width in pixels (will be clamped to a sensible minimum) - */ - public static void setGlobalLabelWidth(final int px) { - globalLabelWidthPx = Math.max(80, px); - } - - private static int computePreferredHeight(final Font font, final int approxLines) { - if (font == null) { - return 40; - } - javax.swing.JLabel probe = new javax.swing.JLabel(); - java.awt.FontMetrics fm = probe.getFontMetrics(font); - int h = fm.getHeight() * Math.max(1, approxLines) + 6; - return Math.max(40, h); - } - - /** - * Return the configured global label width in pixels used by new instances. - * - * @return global label width in pixels - */ - public static int getGlobalLabelWidth() { return globalLabelWidthPx; } - - /** - * Compute the pixel width of the longest label string using the given - * font. Returns the maximum string width in pixels. - * - * @param font font to use when measuring (may be null to use default) - * @param labels array of label texts to measure - * @return maximum string width in pixels (>=0) - */ - public static int computeMaxLabelPixelWidth(final java.awt.Font font, final String[] labels) { - if (labels == null || labels.length == 0) { - return globalLabelWidthPx; - } - javax.swing.JLabel probe = new javax.swing.JLabel(); - java.awt.FontMetrics fm = probe.getFontMetrics(font != null ? font : probe.getFont()); - int max = 0; - for (String s : labels) { - if (s != null) { - max = Math.max(max, fm.stringWidth(s)); - } - } - return max; - } - - /** - * Set the visible label text for this row. - * - * @param text new label text - */ - public void setLabel(final String text) { labelArea.setText(text); } - - /** - * Get the current label text for this field. - * - * @return label text - */ - public String getLabel() { return labelArea.getText(); } - - /** - * Get the integer value currently selected in the spinner. - * - * @return spinner integer value (0..4) - */ - public int getValue() { - // If the user is mid-edit in the spinner's text field, try to commit the edit - try { - java.awt.Component ed = spinner.getEditor(); - if (ed instanceof javax.swing.JSpinner.DefaultEditor editorComp) { - javax.swing.JFormattedTextField tf = editorComp.getTextField(); - try { tf.commitEdit(); } catch (java.text.ParseException pe) { LOG.trace("Spinner editor parse error", pe); } - } - } catch (IllegalArgumentException | IllegalStateException re) { LOG.trace("Unexpected error committing spinner edit", re); } - return (Integer) spinner.getValue(); - } - - /** - * Set the spinner value clamped to the valid range (0..4). - * - * @param v desired spinner value - */ - public void setValue(final int v) { spinner.setValue(Math.max(0, Math.min(4, v))); } - - @Override - public void setName(final String name) { - super.setName(name); - spinner.setName(name); - } - - // Diagnostics: expose spinner X and label wrap width (useful to verify layout) - /** - * Get the X coordinate of the spinner inside this component (pixels). - * - * @return spinner X position in pixels - */ - public int getSpinnerX() { return spinner.getLocation().x; } - - /** - * Return the configured label wrap container's current width in pixels. - * - * @return label wrap width in pixels - */ - public int getLabelWrapWidth() { return labelWrap.getWidth(); } - - /** - * Actual horizontal gap in pixels between the label wrap right edge and the spinner left edge. - * - * @return pixel gap between label and spinner - */ - public int getActualGap() { - int labelRight = labelWrap.getX() + labelWrap.getWidth(); - return spinner.getX() - labelRight; - } -} +package com.studentgui.uicomp; + +import java.awt.Dimension; +import java.awt.Font; +import java.awt.GridBagConstraints; +import java.awt.GridBagLayout; +import java.awt.Insets; + +import javax.swing.BorderFactory; +import javax.swing.JComponent; +import javax.swing.JPanel; +import javax.swing.JSpinner; +import javax.swing.JTextArea; +import javax.swing.SpinnerNumberModel; + +/** + * Reusable component that renders a wrapped descriptive label and a compact + * integer input (0..4). The label is a non-editable JTextArea that wraps at + * ~200px; the component adds a 20px left inset so the label appears offset. + * The spinner is aligned to the first line of the label. + */ +public class PhaseScoreField extends JPanel { + private static final long serialVersionUID = 1L; + private static final org.slf4j.Logger LOG = org.slf4j.LoggerFactory.getLogger(PhaseScoreField.class); + // Ensure we only log the font-adjustment debug once to avoid noisy output in headless tests. + private static final java.util.concurrent.atomic.AtomicBoolean FONT_ADJUST_LOGGED = new java.util.concurrent.atomic.AtomicBoolean(false); + /** Wrapped, read-only label area used to display the description text. */ + private final JTextArea labelArea; + /** Numeric spinner used for 0..4 score entry. */ + private final JSpinner spinner; + /** Container used to constrain the wrap width of the label area. */ + private final JPanel labelWrap; + // Global label width (pixels) used to make all rows align; default ~200 + // Note: intentionally non-final so pages can adjust it at runtime. + private static int globalLabelWidthPx = 200; + + /** Horizontal spacer panel inserted to tune the gap between label and spinner. */ + private final JPanel spacer; + + /** + * Create a PhaseScoreField containing a wrapped label and a numeric spinner. + * + * @param labelText text label to display (may be multi-line) + * @param initial initial integer value for the spinner (0..4) + */ + public PhaseScoreField(final String labelText, final int initial) { + super(new GridBagLayout()); + this.labelArea = new JTextArea(labelText); + labelArea.setLineWrap(true); + labelArea.setWrapStyleWord(true); + labelArea.setEditable(false); + labelArea.setOpaque(false); + labelArea.setFocusable(false); + // Use explicit font so the appearance doesn't change when switching LAFs + Font labelFont = new Font(Font.SANS_SERIF, Font.PLAIN, 12); + labelArea.setFont(labelFont); + // Constrain width to the configured global label width so it doesn't expand. + // Pages set GLOBAL_LABEL_WIDTH_PX to (maxLabelPx + 50). We render the label + // area at (GLOBAL - 50) and insert a 50px spacer so the spinner sits + // exactly 50px after the longest label text. + int prefHeight = computePreferredHeight(labelFont, 2); + int labelWidth = Math.max(40, globalLabelWidthPx - 50); + java.awt.Dimension fixed = new java.awt.Dimension(labelWidth, prefHeight); + // Wrap the JTextArea in a small container to guarantee horizontal size + this.labelWrap = new JPanel(new java.awt.BorderLayout()); + this.labelWrap.setPreferredSize(fixed); + this.labelWrap.setMinimumSize(fixed); + this.labelWrap.setMaximumSize(new java.awt.Dimension(labelWidth, Short.MAX_VALUE)); + this.labelWrap.add(labelArea, java.awt.BorderLayout.CENTER); + + this.spinner = new JSpinner(new SpinnerNumberModel(initial, 0, 4, 1)); + JComponent editor = spinner.getEditor(); + // Set explicit font for spinner editor to keep sizing consistent across themes + Font spinnerFont = new Font(Font.SANS_SERIF, Font.PLAIN, 12); + editor.setFont(spinnerFont); + // The editor is typically a JSpinner.DefaultEditor containing a JTextField + try { + java.lang.reflect.Field f = editor.getClass().getDeclaredField("textField"); + f.setAccessible(true); + Object tf = f.get(editor); + if (tf instanceof javax.swing.JTextField tfField) { + tfField.setFont(spinnerFont); + } + } catch (ReflectiveOperationException roe) { + // Field may not exist on some LAF/editor implementations. Log this at most once + // to avoid excessive noise during automated test runs. + if (FONT_ADJUST_LOGGED.compareAndSet(false, true)) { + LOG.trace("Could not adjust spinner editor textField font (field missing or inaccessible)"); + } + } + editor.setPreferredSize(new Dimension(48, 20)); + spinner.setPreferredSize(new Dimension(48, 24)); + + setBorder(BorderFactory.createEmptyBorder(0, 20, 0, 0)); // left inset 20px + + GridBagConstraints gbc = new GridBagConstraints(); + // Label: fixed preferred width, do not expand horizontally + gbc.gridx = 0; + gbc.gridy = 0; + gbc.anchor = GridBagConstraints.NORTHWEST; + gbc.fill = GridBagConstraints.NONE; // keep label at preferred size + gbc.weightx = 0.0; + gbc.insets = new Insets(2, 2, 2, 8); + add(labelWrap, gbc); + + // Spacer: compute width so the spinner ends up 50px after the rendered label text + gbc.gridx = 1; + gbc.gridy = 0; + gbc.anchor = GridBagConstraints.NORTHWEST; + gbc.fill = GridBagConstraints.NONE; + gbc.weightx = 0.0; + // Compute rendered text pixel width for this label (safe to call here) + int textPx = computeMaxLabelPixelWidth(labelFont, new String[] { labelText }); + int paddingWithinWrap = Math.max(0, labelWidth - textPx); + int spacerWidth = Math.max(0, 50 - paddingWithinWrap); + this.spacer = new JPanel(); this.spacer.setPreferredSize(new java.awt.Dimension(spacerWidth, 1)); + add(this.spacer, gbc); + + // Spinner sits immediately to the right of the spacer + gbc.gridx = 2; + gbc.gridy = 0; + gbc.anchor = GridBagConstraints.NORTHWEST; + gbc.fill = GridBagConstraints.NONE; + gbc.weightx = 0.0; + add(spinner, gbc); + + // Filler: consumes remaining horizontal space so the spinner doesn't get pushed to the far right + gbc.gridx = 3; + gbc.gridy = 0; + gbc.anchor = GridBagConstraints.NORTHWEST; + gbc.fill = GridBagConstraints.HORIZONTAL; + gbc.weightx = 1.0; + add(new JPanel(), gbc); + + // After layout, adjust spacer so the visible gap between label and spinner is exactly 50px + javax.swing.SwingUtilities.invokeLater(() -> { + int labelRight = labelWrap.getX() + labelWrap.getWidth(); + int actualGap = spinner.getX() - labelRight; + int desiredGap = 50; + int currentSpacer = this.spacer.getPreferredSize().width; + int delta = desiredGap - actualGap; + if (delta != 0) { + int newWidth = Math.max(0, currentSpacer + delta); + this.spacer.setPreferredSize(new java.awt.Dimension(newWidth, 1)); + this.spacer.revalidate(); + this.revalidate(); + this.repaint(); + } + }); + } + + /** + * Set a global label width used by all PhaseScoreField instances created + * after calling this method. This helps align the spinner input across + * multiple rows so the entry fields start at a consistent position. + * + * @param px target global label width in pixels (will be clamped to a sensible minimum) + */ + public static void setGlobalLabelWidth(final int px) { + globalLabelWidthPx = Math.max(80, px); + } + + private static int computePreferredHeight(final Font font, final int approxLines) { + if (font == null) { + return 40; + } + javax.swing.JLabel probe = new javax.swing.JLabel(); + java.awt.FontMetrics fm = probe.getFontMetrics(font); + int h = fm.getHeight() * Math.max(1, approxLines) + 6; + return Math.max(40, h); + } + + /** + * Return the configured global label width in pixels used by new instances. + * + * @return global label width in pixels + */ + public static int getGlobalLabelWidth() { return globalLabelWidthPx; } + + /** + * Compute the pixel width of the longest label string using the given + * font. Returns the maximum string width in pixels. + * + * @param font font to use when measuring (may be null to use default) + * @param labels array of label texts to measure + * @return maximum string width in pixels (>=0) + */ + public static int computeMaxLabelPixelWidth(final java.awt.Font font, final String[] labels) { + if (labels == null || labels.length == 0) { + return globalLabelWidthPx; + } + javax.swing.JLabel probe = new javax.swing.JLabel(); + java.awt.FontMetrics fm = probe.getFontMetrics(font != null ? font : probe.getFont()); + int max = 0; + for (String s : labels) { + if (s != null) { + max = Math.max(max, fm.stringWidth(s)); + } + } + return max; + } + + /** + * Set the visible label text for this row. + * + * @param text new label text + */ + public void setLabel(final String text) { labelArea.setText(text); } + + /** + * Get the current label text for this field. + * + * @return label text + */ + public String getLabel() { return labelArea.getText(); } + + /** + * Get the integer value currently selected in the spinner. + * + * @return spinner integer value (0..4) + */ + public int getValue() { + // If the user is mid-edit in the spinner's text field, try to commit the edit + try { + java.awt.Component ed = spinner.getEditor(); + if (ed instanceof javax.swing.JSpinner.DefaultEditor editorComp) { + javax.swing.JFormattedTextField tf = editorComp.getTextField(); + try { tf.commitEdit(); } catch (java.text.ParseException pe) { LOG.trace("Spinner editor parse error", pe); } + } + } catch (IllegalArgumentException | IllegalStateException re) { LOG.trace("Unexpected error committing spinner edit", re); } + return (Integer) spinner.getValue(); + } + + /** + * Set the spinner value clamped to the valid range (0..4). + * + * @param v desired spinner value + */ + public void setValue(final int v) { spinner.setValue(Math.max(0, Math.min(4, v))); } + + @Override + public void setName(final String name) { + super.setName(name); + spinner.setName(name); + } + + // Diagnostics: expose spinner X and label wrap width (useful to verify layout) + /** + * Get the X coordinate of the spinner inside this component (pixels). + * + * @return spinner X position in pixels + */ + public int getSpinnerX() { return spinner.getLocation().x; } + + /** + * Return the configured label wrap container's current width in pixels. + * + * @return label wrap width in pixels + */ + public int getLabelWrapWidth() { return labelWrap.getWidth(); } + + /** + * Actual horizontal gap in pixels between the label wrap right edge and the spinner left edge. + * + * @return pixel gap between label and spinner + */ + public int getActualGap() { + int labelRight = labelWrap.getX() + labelWrap.getWidth(); + return spinner.getX() - labelRight; + } +} diff --git a/src/main/resources/logback.xml b/src/main/resources/logback.xml index ec11e1f..32d8476 100644 --- a/src/main/resources/logback.xml +++ b/src/main/resources/logback.xml @@ -1,50 +1,50 @@ - - - - - - - - - - - - - - - - ${PATTERN} - - - - - - ${APP_HOME}/logs/log_${LOG_TS}.log - - - ${APP_HOME}/logs/log_${LOG_TS}.%d{yyyy-MM-dd}.%i.log - 10MB - 30 - 1GB - - - ${PATTERN} - - - - - - - - - - - - - - + + + + + + + + + + + + + + + + ${PATTERN} + + + + + + ${APP_HOME}/logs/log_${LOG_TS}.log + + + ${APP_HOME}/logs/log_${LOG_TS}.%d{yyyy-MM-dd}.%i.log + 10MB + 30 + 1GB + + + ${PATTERN} + + + + + + + + + + + + + + diff --git a/src/main/resources/version.properties b/src/main/resources/version.properties index a9ca5c5..12adc12 100644 --- a/src/main/resources/version.properties +++ b/src/main/resources/version.properties @@ -1 +1 @@ -version=@project.version@ +version=@project.version@ diff --git a/src/test/java/com/studentgui/apphelpers/DatabaseContactLogTest.java b/src/test/java/com/studentgui/apphelpers/DatabaseContactLogTest.java index 9919e3d..7187df6 100644 --- a/src/test/java/com/studentgui/apphelpers/DatabaseContactLogTest.java +++ b/src/test/java/com/studentgui/apphelpers/DatabaseContactLogTest.java @@ -1,25 +1,25 @@ -package com.studentgui.apphelpers; - -import java.time.LocalDate; - -import static org.junit.jupiter.api.Assertions.assertEquals; -import static org.junit.jupiter.api.Assertions.assertNotNull; -import org.junit.jupiter.api.Test; - -public class DatabaseContactLogTest { - - @Test - public void testSaveAndFetchContactLog() throws Exception { - SqlGenerate.initializeDatabase(); - String student = "Test Student"; - int sid = Database.getOrCreateStudent(student); - int pt = Database.getOrCreateProgressType("ContactLog"); - int sessionId = Database.createProgressSession(sid, pt, LocalDate.now()); - Database.saveContactLog(sessionId, student, LocalDate.now().toString(), "Guardian A", "Phone", "+1234567890", "a@example.com", "Left voicemail", "General summary", "Specific item", "Detailed notes"); - com.studentgui.apphelpers.dto.ContactPayload fetched = Database.fetchLatestContactLog(student); - assertNotNull(fetched); - assertEquals("Guardian A", fetched.guardian); - assertEquals("+1234567890", fetched.phone); - assertEquals("Detailed notes", fetched.notes); - } -} +package com.studentgui.apphelpers; + +import java.time.LocalDate; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNotNull; +import org.junit.jupiter.api.Test; + +public class DatabaseContactLogTest { + + @Test + public void testSaveAndFetchContactLog() throws Exception { + SqlGenerate.initializeDatabase(); + String student = "Test Student"; + int sid = Database.getOrCreateStudent(student); + int pt = Database.getOrCreateProgressType("ContactLog"); + int sessionId = Database.createProgressSession(sid, pt, LocalDate.now()); + Database.saveContactLog(sessionId, student, LocalDate.now().toString(), "Guardian A", "Phone", "+1234567890", "a@example.com", "Left voicemail", "General summary", "Specific item", "Detailed notes"); + com.studentgui.apphelpers.dto.ContactPayload fetched = Database.fetchLatestContactLog(student); + assertNotNull(fetched); + assertEquals("Guardian A", fetched.guardian); + assertEquals("+1234567890", fetched.phone); + assertEquals("Detailed notes", fetched.notes); + } +} diff --git a/src/test/java/com/studentgui/apphelpers/SessionJsonWriterTest.java b/src/test/java/com/studentgui/apphelpers/SessionJsonWriterTest.java index 516054d..4c56bea 100644 --- a/src/test/java/com/studentgui/apphelpers/SessionJsonWriterTest.java +++ b/src/test/java/com/studentgui/apphelpers/SessionJsonWriterTest.java @@ -1,49 +1,49 @@ -package com.studentgui.apphelpers; - -import java.nio.file.Files; -import java.nio.file.Path; - -import static org.junit.jupiter.api.Assertions.assertEquals; -import static org.junit.jupiter.api.Assertions.assertNotNull; -import static org.junit.jupiter.api.Assertions.assertTrue; -import org.junit.jupiter.api.Test; - -import com.fasterxml.jackson.databind.JsonNode; -import com.fasterxml.jackson.databind.ObjectMapper; -import com.studentgui.apphelpers.dto.NotesPayload; - -/** - * Unit test for SessionJsonWriter to verify envelope and filename format. - */ -public class SessionJsonWriterTest { - - @Test - public void writeSessionJson_includesSessionIdAndPayload() throws Exception { - String student = "UnitTestStudent-" + System.nanoTime(); - int sessionId = 314159; - NotesPayload payload = new NotesPayload(sessionId, "unit test notes payload"); - - Path out = SessionJsonWriter.writeSessionJson(student, "UnitTestPage", payload, sessionId); - assertNotNull(out, "writeSessionJson should return a path"); - assertTrue(Files.exists(out), "written file should exist"); - - String fname = out.getFileName().toString(); - assertTrue(fname.contains("UnitTestPage"), "filename should contain page name"); - assertTrue(fname.contains("-session-" + sessionId), "filename should include session id segment"); - - byte[] data = Files.readAllBytes(out); - ObjectMapper m = new ObjectMapper(); - JsonNode root = m.readTree(data); - assertEquals(student, root.get("student").asText()); - assertEquals("UnitTestPage", root.get("page").asText()); - assertTrue(root.has("sessionId")); - assertEquals(sessionId, root.get("sessionId").asInt()); - - JsonNode payloadNode = root.get("payload"); - assertNotNull(payloadNode); - assertEquals("unit test notes payload", payloadNode.get("notes").asText()); - - // cleanup - Files.deleteIfExists throws IOException; catch that specifically - try { Files.deleteIfExists(out); } catch (java.io.IOException ex) { /* best-effort cleanup */ } - } -} +package com.studentgui.apphelpers; + +import java.nio.file.Files; +import java.nio.file.Path; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.junit.jupiter.api.Assertions.assertTrue; +import org.junit.jupiter.api.Test; + +import com.fasterxml.jackson.databind.JsonNode; +import com.fasterxml.jackson.databind.ObjectMapper; +import com.studentgui.apphelpers.dto.NotesPayload; + +/** + * Unit test for SessionJsonWriter to verify envelope and filename format. + */ +public class SessionJsonWriterTest { + + @Test + public void writeSessionJson_includesSessionIdAndPayload() throws Exception { + String student = "UnitTestStudent-" + System.nanoTime(); + int sessionId = 314159; + NotesPayload payload = new NotesPayload(sessionId, "unit test notes payload"); + + Path out = SessionJsonWriter.writeSessionJson(student, "UnitTestPage", payload, sessionId); + assertNotNull(out, "writeSessionJson should return a path"); + assertTrue(Files.exists(out), "written file should exist"); + + String fname = out.getFileName().toString(); + assertTrue(fname.contains("UnitTestPage"), "filename should contain page name"); + assertTrue(fname.contains("-session-" + sessionId), "filename should include session id segment"); + + byte[] data = Files.readAllBytes(out); + ObjectMapper m = new ObjectMapper(); + JsonNode root = m.readTree(data); + assertEquals(student, root.get("student").asText()); + assertEquals("UnitTestPage", root.get("page").asText()); + assertTrue(root.has("sessionId")); + assertEquals(sessionId, root.get("sessionId").asInt()); + + JsonNode payloadNode = root.get("payload"); + assertNotNull(payloadNode); + assertEquals("unit test notes payload", payloadNode.get("notes").asText()); + + // cleanup - Files.deleteIfExists throws IOException; catch that specifically + try { Files.deleteIfExists(out); } catch (java.io.IOException ex) { /* best-effort cleanup */ } + } +} diff --git a/src/test/java/com/studentgui/apphelpers/SqlGenerateTest.java b/src/test/java/com/studentgui/apphelpers/SqlGenerateTest.java index be277ac..9465de3 100644 --- a/src/test/java/com/studentgui/apphelpers/SqlGenerateTest.java +++ b/src/test/java/com/studentgui/apphelpers/SqlGenerateTest.java @@ -1,28 +1,28 @@ -package com.studentgui.apphelpers; - -import java.nio.file.Files; -import java.nio.file.Path; -import java.sql.Connection; -import java.sql.DriverManager; -import java.sql.ResultSet; -import java.sql.Statement; - -import static org.junit.jupiter.api.Assertions.assertTrue; -import org.junit.jupiter.api.Test; - -public class SqlGenerateTest { - - @Test - public void testInitializeCreatesContactLogTable() throws Exception { - SqlGenerate.initializeDatabase(); - Path db = Helpers.DATABASE_PATH; - assertTrue(Files.exists(db)); - String url = "jdbc:sqlite:" + db.toString(); - try (Connection c = DriverManager.getConnection(url)) { - try (Statement st = c.createStatement()) { - ResultSet rs = st.executeQuery("SELECT name FROM sqlite_master WHERE type='table' AND name='ContactLog'"); - assertTrue(rs.next(), "ContactLog table should exist after initialization"); - } - } - } -} +package com.studentgui.apphelpers; + +import java.nio.file.Files; +import java.nio.file.Path; +import java.sql.Connection; +import java.sql.DriverManager; +import java.sql.ResultSet; +import java.sql.Statement; + +import static org.junit.jupiter.api.Assertions.assertTrue; +import org.junit.jupiter.api.Test; + +public class SqlGenerateTest { + + @Test + public void testInitializeCreatesContactLogTable() throws Exception { + SqlGenerate.initializeDatabase(); + Path db = Helpers.DATABASE_PATH; + assertTrue(Files.exists(db)); + String url = "jdbc:sqlite:" + db.toString(); + try (Connection c = DriverManager.getConnection(url)) { + try (Statement st = c.createStatement()) { + ResultSet rs = st.executeQuery("SELECT name FROM sqlite_master WHERE type='table' AND name='ContactLog'"); + assertTrue(rs.next(), "ContactLog table should exist after initialization"); + } + } + } +} diff --git a/src/test/java/com/studentgui/apppages/JLineGraphDeterministicJitterTest.java b/src/test/java/com/studentgui/apppages/JLineGraphDeterministicJitterTest.java index 87de98b..032340e 100644 --- a/src/test/java/com/studentgui/apppages/JLineGraphDeterministicJitterTest.java +++ b/src/test/java/com/studentgui/apppages/JLineGraphDeterministicJitterTest.java @@ -1,42 +1,42 @@ -package com.studentgui.apppages; - -import java.lang.reflect.Method; - -import static org.junit.jupiter.api.Assertions.assertArrayEquals; -import org.junit.jupiter.api.Test; - -/** - * Small unit test to validate deterministic jitter reproducibility. - * The test uses reflection to invoke the private addJitter(double) helper - * on two separate JLineGraph instances configured with the same seed and - * deterministic mode. The produced sequences must match exactly. - */ -public class JLineGraphDeterministicJitterTest { - - @Test - public void deterministicJitterProducesSameSequence() throws Exception { - JLineGraph g1 = new JLineGraph(); - JLineGraph g2 = new JLineGraph(); - - g1.setJitterDeterministic(true); - g2.setJitterDeterministic(true); - g1.setJitterSeed(123456789L); - g2.setJitterSeed(123456789L); - - Method addJitter = JLineGraph.class.getDeclaredMethod("addJitter", double.class); - addJitter.setAccessible(true); - - final int N = 10; - double[] seq1 = new double[N]; - double[] seq2 = new double[N]; - - double base = 2.0; - for (int i = 0; i < N; i++) { - seq1[i] = (double) addJitter.invoke(g1, base); - seq2[i] = (double) addJitter.invoke(g2, base); - } - - // sequences must match exactly when using same seed - assertArrayEquals(seq1, seq2, 0.0); - } -} +package com.studentgui.apppages; + +import java.lang.reflect.Method; + +import static org.junit.jupiter.api.Assertions.assertArrayEquals; +import org.junit.jupiter.api.Test; + +/** + * Small unit test to validate deterministic jitter reproducibility. + * The test uses reflection to invoke the private addJitter(double) helper + * on two separate JLineGraph instances configured with the same seed and + * deterministic mode. The produced sequences must match exactly. + */ +public class JLineGraphDeterministicJitterTest { + + @Test + public void deterministicJitterProducesSameSequence() throws Exception { + JLineGraph g1 = new JLineGraph(); + JLineGraph g2 = new JLineGraph(); + + g1.setJitterDeterministic(true); + g2.setJitterDeterministic(true); + g1.setJitterSeed(123456789L); + g2.setJitterSeed(123456789L); + + Method addJitter = JLineGraph.class.getDeclaredMethod("addJitter", double.class); + addJitter.setAccessible(true); + + final int N = 10; + double[] seq1 = new double[N]; + double[] seq2 = new double[N]; + + double base = 2.0; + for (int i = 0; i < N; i++) { + seq1[i] = (double) addJitter.invoke(g1, base); + seq2[i] = (double) addJitter.invoke(g2, base); + } + + // sequences must match exactly when using same seed + assertArrayEquals(seq1, seq2, 0.0); + } +} diff --git a/src/test/java/com/studentgui/test/BrailleDatabaseTest.java b/src/test/java/com/studentgui/test/BrailleDatabaseTest.java index 1ad6146..f3369e5 100644 --- a/src/test/java/com/studentgui/test/BrailleDatabaseTest.java +++ b/src/test/java/com/studentgui/test/BrailleDatabaseTest.java @@ -1,48 +1,48 @@ -package com.studentgui.test; - -import java.time.LocalDate; -import java.util.List; - -import static org.junit.jupiter.api.Assertions.assertNotNull; -import static org.junit.jupiter.api.Assertions.assertTrue; -import org.junit.jupiter.api.Test; - -import com.studentgui.apphelpers.Helpers; -import com.studentgui.apphelpers.SqlGenerate; - -/** - * Small integration-style unit test that uses the normalized Database helper methods - * to create a student, a progress type, ensure parts, insert one session and fetch the - * latest results. This runs headless and doesn't start any UI components. - */ -public class BrailleDatabaseTest { - - @Test - public void smokeDatabaseFlow() throws Exception { - // Ensure app folders and DB exist - Helpers.createFolderHierarchy(); - SqlGenerate.initializeDatabase(); - - int studentId = com.studentgui.apphelpers.Database.getOrCreateStudent("JUnit Smoke Student"); - assertTrue(studentId > 0); - - int ptId = com.studentgui.apphelpers.Database.getOrCreateProgressType("Braille"); - assertTrue(ptId > 0); - - String[] codes = new String[5]; - int[] scores = new int[5]; - for (int i = 0; i < 5; i++) { codes[i] = "P" + (i+1); scores[i] = (i % 3) + 1; } - com.studentgui.apphelpers.Database.ensureAssessmentParts(ptId, codes); - - int sessionId = com.studentgui.apphelpers.Database.createProgressSession(studentId, ptId, LocalDate.now()); - assertTrue(sessionId > 0); - - com.studentgui.apphelpers.Database.insertAssessmentResults(sessionId, ptId, codes, scores); - - List> rows = com.studentgui.apphelpers.Database.fetchLatestAssessmentResults("JUnit Smoke Student", "Braille", 5); - assertNotNull(rows); - - // At least one row should be returned - assertTrue(rows.size() >= 1); - } -} +package com.studentgui.test; + +import java.time.LocalDate; +import java.util.List; + +import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.junit.jupiter.api.Assertions.assertTrue; +import org.junit.jupiter.api.Test; + +import com.studentgui.apphelpers.Helpers; +import com.studentgui.apphelpers.SqlGenerate; + +/** + * Small integration-style unit test that uses the normalized Database helper methods + * to create a student, a progress type, ensure parts, insert one session and fetch the + * latest results. This runs headless and doesn't start any UI components. + */ +public class BrailleDatabaseTest { + + @Test + public void smokeDatabaseFlow() throws Exception { + // Ensure app folders and DB exist + Helpers.createFolderHierarchy(); + SqlGenerate.initializeDatabase(); + + int studentId = com.studentgui.apphelpers.Database.getOrCreateStudent("JUnit Smoke Student"); + assertTrue(studentId > 0); + + int ptId = com.studentgui.apphelpers.Database.getOrCreateProgressType("Braille"); + assertTrue(ptId > 0); + + String[] codes = new String[5]; + int[] scores = new int[5]; + for (int i = 0; i < 5; i++) { codes[i] = "P" + (i+1); scores[i] = (i % 3) + 1; } + com.studentgui.apphelpers.Database.ensureAssessmentParts(ptId, codes); + + int sessionId = com.studentgui.apphelpers.Database.createProgressSession(studentId, ptId, LocalDate.now()); + assertTrue(sessionId > 0); + + com.studentgui.apphelpers.Database.insertAssessmentResults(sessionId, ptId, codes, scores); + + List> rows = com.studentgui.apphelpers.Database.fetchLatestAssessmentResults("JUnit Smoke Student", "Braille", 5); + assertNotNull(rows); + + // At least one row should be returned + assertTrue(rows.size() >= 1); + } +} diff --git a/src/test/java/com/studentgui/test/BrailleSmokeTest.java b/src/test/java/com/studentgui/test/BrailleSmokeTest.java index 7e56dc8..c7118f2 100644 --- a/src/test/java/com/studentgui/test/BrailleSmokeTest.java +++ b/src/test/java/com/studentgui/test/BrailleSmokeTest.java @@ -1,42 +1,42 @@ -package com.studentgui.test; - -import static org.junit.jupiter.api.Assertions.assertNotNull; - -import java.time.LocalDate; -import java.util.List; - -import org.junit.jupiter.api.Test; - -import com.studentgui.apphelpers.Helpers; -import com.studentgui.apphelpers.SqlGenerate; -import com.studentgui.apppages.JLineGraph; - -/** - * JUnit replacement for the legacy Braille smoke main. Exercises the - * normalized database APIs and invokes JLineGraph.updateWithData(...) to - * verify plumbing without launching the full GUI. - */ -public class BrailleSmokeTest { - - @Test - public void smokeTestDatabaseAndGraph() throws Exception { - Helpers.createFolderHierarchy(); - SqlGenerate.initializeDatabase(); - - int studentId = com.studentgui.apphelpers.Database.getOrCreateStudent("JUnit Smoke Student"); - int ptId = com.studentgui.apphelpers.Database.getOrCreateProgressType("Braille"); - - String[] codes = new String[28]; - int[] scores = new int[28]; - for (int i = 0; i < 28; i++) { codes[i] = "P" + (i+1); scores[i] = (i % 5) + 1; } - com.studentgui.apphelpers.Database.ensureAssessmentParts(ptId, codes); - int sessionId = com.studentgui.apphelpers.Database.createProgressSession(studentId, ptId, LocalDate.now()); - com.studentgui.apphelpers.Database.insertAssessmentResults(sessionId, ptId, codes, scores); - - List> allSkillValues = com.studentgui.apphelpers.Database.fetchLatestAssessmentResults("JUnit Smoke Student", "Braille", 5); - assertNotNull(allSkillValues); - - JLineGraph graph = new JLineGraph(); - graph.updateWithData(allSkillValues); - } -} +package com.studentgui.test; + +import static org.junit.jupiter.api.Assertions.assertNotNull; + +import java.time.LocalDate; +import java.util.List; + +import org.junit.jupiter.api.Test; + +import com.studentgui.apphelpers.Helpers; +import com.studentgui.apphelpers.SqlGenerate; +import com.studentgui.apppages.JLineGraph; + +/** + * JUnit replacement for the legacy Braille smoke main. Exercises the + * normalized database APIs and invokes JLineGraph.updateWithData(...) to + * verify plumbing without launching the full GUI. + */ +public class BrailleSmokeTest { + + @Test + public void smokeTestDatabaseAndGraph() throws Exception { + Helpers.createFolderHierarchy(); + SqlGenerate.initializeDatabase(); + + int studentId = com.studentgui.apphelpers.Database.getOrCreateStudent("JUnit Smoke Student"); + int ptId = com.studentgui.apphelpers.Database.getOrCreateProgressType("Braille"); + + String[] codes = new String[28]; + int[] scores = new int[28]; + for (int i = 0; i < 28; i++) { codes[i] = "P" + (i+1); scores[i] = (i % 5) + 1; } + com.studentgui.apphelpers.Database.ensureAssessmentParts(ptId, codes); + int sessionId = com.studentgui.apphelpers.Database.createProgressSession(studentId, ptId, LocalDate.now()); + com.studentgui.apphelpers.Database.insertAssessmentResults(sessionId, ptId, codes, scores); + + List> allSkillValues = com.studentgui.apphelpers.Database.fetchLatestAssessmentResults("JUnit Smoke Student", "Braille", 5); + assertNotNull(allSkillValues); + + JLineGraph graph = new JLineGraph(); + graph.updateWithData(allSkillValues); + } +} diff --git a/src/test/java/com/studentgui/test/DatabaseEdgeCasesTest.java b/src/test/java/com/studentgui/test/DatabaseEdgeCasesTest.java index bb1d399..dcc8e48 100644 --- a/src/test/java/com/studentgui/test/DatabaseEdgeCasesTest.java +++ b/src/test/java/com/studentgui/test/DatabaseEdgeCasesTest.java @@ -1,58 +1,58 @@ -package com.studentgui.test; - -import static org.junit.jupiter.api.Assertions.*; - -import java.time.LocalDate; -import java.util.List; - -import org.junit.jupiter.api.BeforeAll; -import org.junit.jupiter.api.Test; - -import com.studentgui.apphelpers.Helpers; -import com.studentgui.apphelpers.SqlGenerate; - -public class DatabaseEdgeCasesTest { - - @BeforeAll - public static void init() throws Exception { - Helpers.createFolderHierarchy(); - SqlGenerate.initializeDatabase(); - } - - @Test - public void duplicateStudentNamesReturnSameId() throws Exception { - int a = com.studentgui.apphelpers.Database.getOrCreateStudent("Dup Student"); - int b = com.studentgui.apphelpers.Database.getOrCreateStudent("Dup Student"); - assertEquals(a, b, "Duplicate student names should return the same id"); - } - - @Test - public void ensureAssessmentPartsIsIdempotentAndIgnoresUnknownPartsOnInsert() throws Exception { - int pt = com.studentgui.apphelpers.Database.getOrCreateProgressType("EdgeType"); - String[] parts = new String[] {"X1","X2","X3"}; - com.studentgui.apphelpers.Database.ensureAssessmentParts(pt, parts); - // calling again should not fail and should be idempotent - com.studentgui.apphelpers.Database.ensureAssessmentParts(pt, parts); - - int sid = com.studentgui.apphelpers.Database.getOrCreateStudent("Edge Student"); - int session = com.studentgui.apphelpers.Database.createProgressSession(sid, pt, LocalDate.now()); - // insert with an unknown part code - should be ignored, no exception - com.studentgui.apphelpers.Database.insertAssessmentResults(session, pt, new String[] {"X1","UNKNOWN"}, new int[] {5, 9}); - - List> rows = com.studentgui.apphelpers.Database.fetchLatestAssessmentResults("Edge Student", "EdgeType", 5); - assertNotNull(rows); - assertTrue(rows.size() >= 1); - } - - @Test - public void saveSessionNotesPersistsNotes() throws Exception { - int pt = com.studentgui.apphelpers.Database.getOrCreateProgressType("NoteType"); - int sid = com.studentgui.apphelpers.Database.getOrCreateStudent("Notes Student"); - int session = com.studentgui.apphelpers.Database.createProgressSession(sid, pt, LocalDate.now()); - com.studentgui.apphelpers.Database.saveSessionNotes(session, "These are test notes"); - - List> rows = com.studentgui.apphelpers.Database.fetchLatestAssessmentResults("Notes Student", "NoteType", 5); - // fetchLatestAssessmentResults doesn't return notes, but we can at least ensure the session exists by getting session rows - assertNotNull(rows); - } -} +package com.studentgui.test; + +import static org.junit.jupiter.api.Assertions.*; + +import java.time.LocalDate; +import java.util.List; + +import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.Test; + +import com.studentgui.apphelpers.Helpers; +import com.studentgui.apphelpers.SqlGenerate; + +public class DatabaseEdgeCasesTest { + + @BeforeAll + public static void init() throws Exception { + Helpers.createFolderHierarchy(); + SqlGenerate.initializeDatabase(); + } + + @Test + public void duplicateStudentNamesReturnSameId() throws Exception { + int a = com.studentgui.apphelpers.Database.getOrCreateStudent("Dup Student"); + int b = com.studentgui.apphelpers.Database.getOrCreateStudent("Dup Student"); + assertEquals(a, b, "Duplicate student names should return the same id"); + } + + @Test + public void ensureAssessmentPartsIsIdempotentAndIgnoresUnknownPartsOnInsert() throws Exception { + int pt = com.studentgui.apphelpers.Database.getOrCreateProgressType("EdgeType"); + String[] parts = new String[] {"X1","X2","X3"}; + com.studentgui.apphelpers.Database.ensureAssessmentParts(pt, parts); + // calling again should not fail and should be idempotent + com.studentgui.apphelpers.Database.ensureAssessmentParts(pt, parts); + + int sid = com.studentgui.apphelpers.Database.getOrCreateStudent("Edge Student"); + int session = com.studentgui.apphelpers.Database.createProgressSession(sid, pt, LocalDate.now()); + // insert with an unknown part code - should be ignored, no exception + com.studentgui.apphelpers.Database.insertAssessmentResults(session, pt, new String[] {"X1","UNKNOWN"}, new int[] {5, 9}); + + List> rows = com.studentgui.apphelpers.Database.fetchLatestAssessmentResults("Edge Student", "EdgeType", 5); + assertNotNull(rows); + assertTrue(rows.size() >= 1); + } + + @Test + public void saveSessionNotesPersistsNotes() throws Exception { + int pt = com.studentgui.apphelpers.Database.getOrCreateProgressType("NoteType"); + int sid = com.studentgui.apphelpers.Database.getOrCreateStudent("Notes Student"); + int session = com.studentgui.apphelpers.Database.createProgressSession(sid, pt, LocalDate.now()); + com.studentgui.apphelpers.Database.saveSessionNotes(session, "These are test notes"); + + List> rows = com.studentgui.apphelpers.Database.fetchLatestAssessmentResults("Notes Student", "NoteType", 5); + // fetchLatestAssessmentResults doesn't return notes, but we can at least ensure the session exists by getting session rows + assertNotNull(rows); + } +} diff --git a/src/test/java/com/studentgui/test/DatabaseTest.java b/src/test/java/com/studentgui/test/DatabaseTest.java index 41f153a..8b55108 100644 --- a/src/test/java/com/studentgui/test/DatabaseTest.java +++ b/src/test/java/com/studentgui/test/DatabaseTest.java @@ -1,48 +1,48 @@ -package com.studentgui.test; - -import static org.junit.jupiter.api.Assertions.*; - -import java.time.LocalDate; -import java.util.List; - -import org.junit.jupiter.api.BeforeAll; -import org.junit.jupiter.api.Test; - -import com.studentgui.apphelpers.Helpers; -import com.studentgui.apphelpers.SqlGenerate; - -/** - * Basic integration tests for the Database helper using the on-disk sqlite - * created in the project's application data folder. These tests are small and - * intentionally exercise CRUD paths used by the UI pages. - */ -public class DatabaseTest { - - @BeforeAll - public static void init() throws Exception { - Helpers.createFolderHierarchy(); - SqlGenerate.initializeDatabase(); - } - - @Test - public void testStudentCreateAndFetch() throws Exception { - int sid = com.studentgui.apphelpers.Database.getOrCreateStudent("Test Student A"); - assertTrue(sid > 0); - - int ptId = com.studentgui.apphelpers.Database.getOrCreateProgressType("TestType"); - assertTrue(ptId > 0); - - String[] parts = new String[] {"P1","P2","P3"}; - com.studentgui.apphelpers.Database.ensureAssessmentParts(ptId, parts); - - int sessionId = com.studentgui.apphelpers.Database.createProgressSession(sid, ptId, LocalDate.now()); - assertTrue(sessionId > 0); - - int[] scores = new int[] {1,2,3}; - com.studentgui.apphelpers.Database.insertAssessmentResults(sessionId, ptId, parts, scores); - - List> results = com.studentgui.apphelpers.Database.fetchLatestAssessmentResults("Test Student A", "TestType", 5); - assertNotNull(results); - assertTrue(results.size() >= 1); - } -} +package com.studentgui.test; + +import static org.junit.jupiter.api.Assertions.*; + +import java.time.LocalDate; +import java.util.List; + +import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.Test; + +import com.studentgui.apphelpers.Helpers; +import com.studentgui.apphelpers.SqlGenerate; + +/** + * Basic integration tests for the Database helper using the on-disk sqlite + * created in the project's application data folder. These tests are small and + * intentionally exercise CRUD paths used by the UI pages. + */ +public class DatabaseTest { + + @BeforeAll + public static void init() throws Exception { + Helpers.createFolderHierarchy(); + SqlGenerate.initializeDatabase(); + } + + @Test + public void testStudentCreateAndFetch() throws Exception { + int sid = com.studentgui.apphelpers.Database.getOrCreateStudent("Test Student A"); + assertTrue(sid > 0); + + int ptId = com.studentgui.apphelpers.Database.getOrCreateProgressType("TestType"); + assertTrue(ptId > 0); + + String[] parts = new String[] {"P1","P2","P3"}; + com.studentgui.apphelpers.Database.ensureAssessmentParts(ptId, parts); + + int sessionId = com.studentgui.apphelpers.Database.createProgressSession(sid, ptId, LocalDate.now()); + assertTrue(sessionId > 0); + + int[] scores = new int[] {1,2,3}; + com.studentgui.apphelpers.Database.insertAssessmentResults(sessionId, ptId, parts, scores); + + List> results = com.studentgui.apphelpers.Database.fetchLatestAssessmentResults("Test Student A", "TestType", 5); + assertNotNull(results); + assertTrue(results.size() >= 1); + } +} diff --git a/src/test/java/com/studentgui/test/ExportBrailleReportsTest.java b/src/test/java/com/studentgui/test/ExportBrailleReportsTest.java index 2fd706b..6fbc3e2 100644 --- a/src/test/java/com/studentgui/test/ExportBrailleReportsTest.java +++ b/src/test/java/com/studentgui/test/ExportBrailleReportsTest.java @@ -1,126 +1,126 @@ -package com.studentgui.test; - -import java.lang.reflect.Field; -import java.nio.file.Path; -import java.time.LocalDate; -import java.util.Map; - -import static org.junit.jupiter.api.Assertions.assertTrue; -import org.junit.jupiter.api.Test; - -import com.studentgui.apphelpers.Database; -import com.studentgui.apphelpers.Helpers; -import com.studentgui.apppages.Braille; -import com.studentgui.apppages.JLineGraph; - -/** - * Test that generates example Braille exports (per-phase PNGs + MD/HTML) - * for the student "Test Student". This mirrors the export logic used in - * the Braille page submit handler but runs headlessly as a test so the - * agent can produce example files for review. - */ -public class ExportBrailleReportsTest { - - @Test - public void generateBrailleExport() throws Exception { - // Force headless mode for chart rendering in CI-like environments - System.setProperty("java.awt.headless", "true"); - - Helpers.createFolderHierarchy(); - // Ensure DB exists and schema is initialized (idempotent) - com.studentgui.apphelpers.SqlGenerate.initializeDatabase(); - - String student = "Test Student"; - String progressType = "Braille"; - - // Instantiate the Braille page to ensure canonical parts are created - JLineGraph graph = new JLineGraph(); - Braille braille = new Braille(student, LocalDate.now(), graph); - - // Fetch historical rows + dates - Database.ResultsWithDates rwd = Database.fetchLatestAssessmentResultsWithDates(student, progressType, Integer.MAX_VALUE); - - // Reflectively obtain the partCodes and human labels from Braille instance - Field pcField = Braille.class.getDeclaredField("partCodes"); - pcField.setAccessible(true); - String[] partCodes = (String[]) pcField.get(braille); - - Field partsField = Braille.class.getDeclaredField("parts"); - partsField.setAccessible(true); - String[][] parts = (String[][]) partsField.get(braille); - String[] labels = new String[parts.length]; - for (int i = 0; i < parts.length; i++) labels[i] = parts[i][1]; - - java.nio.file.Path out = Helpers.APP_HOME.resolve("StudentDataFiles").resolve(Helpers.safeName(student)).resolve("plots"); - java.nio.file.Files.createDirectories(out); - String baseName = "Braille-example-" + java.time.LocalDate.now().toString(); - - if (rwd != null && rwd.rows != null && !rwd.rows.isEmpty()) { - graph.updateWithGroupedDataByDate(rwd.dates, rwd.rows, partCodes, labels); - } else { - // No historical data; create a single-row from zeros by reflection of Braille's fields - java.util.List> rowsList = new java.util.ArrayList<>(); - java.util.List latest = new java.util.ArrayList<>(); - for (int i = 0; i < partCodes.length; i++) latest.add(0); - rowsList.add(latest); - graph.updateWithGroupedData(rowsList, partCodes); - } - - Map groups = graph.saveGroupedCharts(out, baseName, 1000, 240); - - // Build simple markdown and html reports (reuse palette from JLineGraph) - StringBuilder md = new StringBuilder(); - md.append("# ").append(student).append(" - ").append(java.time.LocalDate.now().toString()).append("\n\n"); - for (Map.Entry e : groups.entrySet()) { - md.append("## ").append(e.getKey()).append("\n\n"); - md.append("![](./").append(e.getValue().getFileName().toString()).append(")\n\n"); - } - java.nio.file.Path mdFile = out.resolve(baseName + ".md"); - java.nio.file.Files.writeString(mdFile, md.toString(), java.nio.charset.StandardCharsets.UTF_8); - - String[] palette = JLineGraph.PALETTE_HEX; - java.util.LinkedHashMap> groupsIdx = new java.util.LinkedHashMap<>(); - for (int i = 0; i < partCodes.length; i++) { - String code = partCodes[i]; - String grp = code != null && code.contains("_") ? code.split("_")[0] : code; - groupsIdx.computeIfAbsent(grp, k -> new java.util.ArrayList<>()).add(i); - } - - StringBuilder html = new StringBuilder(); - html.append(""); - html.append(student).append(" - ").append(java.time.LocalDate.now().toString()).append(""); - html.append(""); - html.append(""); - html.append("

").append(student).append(" - ").append(java.time.LocalDate.now().toString()).append("

"); - for (Map.Entry e2 : groups.entrySet()) { - String grp = e2.getKey(); - String imgName = e2.getValue().getFileName().toString(); - html.append("

").append(grp).append("

"); - html.append("
\"").append(grp).append("\"
"); - java.util.List idxs = groupsIdx.getOrDefault(grp, new java.util.ArrayList<>()); - html.append("
"); - for (int s = 0; s < idxs.size(); s++) { - int idx = idxs.get(s); - String code = partCodes[idx]; - String human = labels[idx]; - String seriesName = code + " - " + human; - String color = palette[s % palette.length]; - html.append("
"); - html.append(""); - html.append("
").append(seriesName).append("
"); - } - html.append("
"); - } - html.append(""); - java.nio.file.Path htmlFile = out.resolve(baseName + ".html"); - java.nio.file.Files.writeString(htmlFile, html.toString(), java.nio.charset.StandardCharsets.UTF_8); - - System.out.println("Exported Braille report to: " + out.toAbsolutePath().toString()); - for (Map.Entry e : groups.entrySet()) System.out.println(" - " + e.getKey() + " -> " + e.getValue().getFileName()); - System.out.println("MD: " + mdFile.getFileName() + " HTML: " + htmlFile.getFileName()); - - // Quick assertion to ensure at least one image or the md file exists - assertTrue(java.nio.file.Files.exists(mdFile)); - } -} +package com.studentgui.test; + +import java.lang.reflect.Field; +import java.nio.file.Path; +import java.time.LocalDate; +import java.util.Map; + +import static org.junit.jupiter.api.Assertions.assertTrue; +import org.junit.jupiter.api.Test; + +import com.studentgui.apphelpers.Database; +import com.studentgui.apphelpers.Helpers; +import com.studentgui.apppages.Braille; +import com.studentgui.apppages.JLineGraph; + +/** + * Test that generates example Braille exports (per-phase PNGs + MD/HTML) + * for the student "Test Student". This mirrors the export logic used in + * the Braille page submit handler but runs headlessly as a test so the + * agent can produce example files for review. + */ +public class ExportBrailleReportsTest { + + @Test + public void generateBrailleExport() throws Exception { + // Force headless mode for chart rendering in CI-like environments + System.setProperty("java.awt.headless", "true"); + + Helpers.createFolderHierarchy(); + // Ensure DB exists and schema is initialized (idempotent) + com.studentgui.apphelpers.SqlGenerate.initializeDatabase(); + + String student = "Test Student"; + String progressType = "Braille"; + + // Instantiate the Braille page to ensure canonical parts are created + JLineGraph graph = new JLineGraph(); + Braille braille = new Braille(student, LocalDate.now(), graph); + + // Fetch historical rows + dates + Database.ResultsWithDates rwd = Database.fetchLatestAssessmentResultsWithDates(student, progressType, Integer.MAX_VALUE); + + // Reflectively obtain the partCodes and human labels from Braille instance + Field pcField = Braille.class.getDeclaredField("partCodes"); + pcField.setAccessible(true); + String[] partCodes = (String[]) pcField.get(braille); + + Field partsField = Braille.class.getDeclaredField("parts"); + partsField.setAccessible(true); + String[][] parts = (String[][]) partsField.get(braille); + String[] labels = new String[parts.length]; + for (int i = 0; i < parts.length; i++) labels[i] = parts[i][1]; + + java.nio.file.Path out = Helpers.APP_HOME.resolve("StudentDataFiles").resolve(Helpers.safeName(student)).resolve("plots"); + java.nio.file.Files.createDirectories(out); + String baseName = "Braille-example-" + java.time.LocalDate.now().toString(); + + if (rwd != null && rwd.rows != null && !rwd.rows.isEmpty()) { + graph.updateWithGroupedDataByDate(rwd.dates, rwd.rows, partCodes, labels); + } else { + // No historical data; create a single-row from zeros by reflection of Braille's fields + java.util.List> rowsList = new java.util.ArrayList<>(); + java.util.List latest = new java.util.ArrayList<>(); + for (int i = 0; i < partCodes.length; i++) latest.add(0); + rowsList.add(latest); + graph.updateWithGroupedData(rowsList, partCodes); + } + + Map groups = graph.saveGroupedCharts(out, baseName, 1000, 240); + + // Build simple markdown and html reports (reuse palette from JLineGraph) + StringBuilder md = new StringBuilder(); + md.append("# ").append(student).append(" - ").append(java.time.LocalDate.now().toString()).append("\n\n"); + for (Map.Entry e : groups.entrySet()) { + md.append("## ").append(e.getKey()).append("\n\n"); + md.append("![](./").append(e.getValue().getFileName().toString()).append(")\n\n"); + } + java.nio.file.Path mdFile = out.resolve(baseName + ".md"); + java.nio.file.Files.writeString(mdFile, md.toString(), java.nio.charset.StandardCharsets.UTF_8); + + String[] palette = JLineGraph.PALETTE_HEX; + java.util.LinkedHashMap> groupsIdx = new java.util.LinkedHashMap<>(); + for (int i = 0; i < partCodes.length; i++) { + String code = partCodes[i]; + String grp = code != null && code.contains("_") ? code.split("_")[0] : code; + groupsIdx.computeIfAbsent(grp, k -> new java.util.ArrayList<>()).add(i); + } + + StringBuilder html = new StringBuilder(); + html.append(""); + html.append(student).append(" - ").append(java.time.LocalDate.now().toString()).append(""); + html.append(""); + html.append(""); + html.append("

").append(student).append(" - ").append(java.time.LocalDate.now().toString()).append("

"); + for (Map.Entry e2 : groups.entrySet()) { + String grp = e2.getKey(); + String imgName = e2.getValue().getFileName().toString(); + html.append("

").append(grp).append("

"); + html.append("
\"").append(grp).append("\"
"); + java.util.List idxs = groupsIdx.getOrDefault(grp, new java.util.ArrayList<>()); + html.append("
"); + for (int s = 0; s < idxs.size(); s++) { + int idx = idxs.get(s); + String code = partCodes[idx]; + String human = labels[idx]; + String seriesName = code + " - " + human; + String color = palette[s % palette.length]; + html.append("
"); + html.append(""); + html.append("
").append(seriesName).append("
"); + } + html.append("
"); + } + html.append(""); + java.nio.file.Path htmlFile = out.resolve(baseName + ".html"); + java.nio.file.Files.writeString(htmlFile, html.toString(), java.nio.charset.StandardCharsets.UTF_8); + + System.out.println("Exported Braille report to: " + out.toAbsolutePath().toString()); + for (Map.Entry e : groups.entrySet()) System.out.println(" - " + e.getKey() + " -> " + e.getValue().getFileName()); + System.out.println("MD: " + mdFile.getFileName() + " HTML: " + htmlFile.getFileName()); + + // Quick assertion to ensure at least one image or the md file exists + assertTrue(java.nio.file.Files.exists(mdFile)); + } +} diff --git a/target/apidocs/legal/ADDITIONAL_LICENSE_INFO b/target/apidocs/legal/ADDITIONAL_LICENSE_INFO deleted file mode 100644 index b62cc3e..0000000 --- a/target/apidocs/legal/ADDITIONAL_LICENSE_INFO +++ /dev/null @@ -1 +0,0 @@ -Please see ..\java.base\ADDITIONAL_LICENSE_INFO diff --git a/target/apidocs/legal/ASSEMBLY_EXCEPTION b/target/apidocs/legal/ASSEMBLY_EXCEPTION deleted file mode 100644 index 0d4cfb4..0000000 --- a/target/apidocs/legal/ASSEMBLY_EXCEPTION +++ /dev/null @@ -1 +0,0 @@ -Please see ..\java.base\ASSEMBLY_EXCEPTION diff --git a/target/apidocs/legal/LICENSE b/target/apidocs/legal/LICENSE deleted file mode 100644 index 4ad9fe4..0000000 --- a/target/apidocs/legal/LICENSE +++ /dev/null @@ -1 +0,0 @@ -Please see ..\java.base\LICENSE diff --git a/target/checkstyle-cachefile b/target/checkstyle-cachefile new file mode 100644 index 0000000..37d3c50 --- /dev/null +++ b/target/checkstyle-cachefile @@ -0,0 +1,9 @@ +#Wed Dec 17 12:44:40 MST 2025 +/run/media/ryhunsaker/250GB/GitHubRepos/StudentGUI_java/src/main/java/com/studentgui/app/DateChangeListener.java=1761321174000 +/run/media/ryhunsaker/250GB/GitHubRepos/StudentGUI_java/src/main/java/com/studentgui/app/SettingsChangeListener.java=1761598886000 +/run/media/ryhunsaker/250GB/GitHubRepos/StudentGUI_java/src/main/java/com/studentgui/app/StudentChangeListener.java=1761331974000 +/run/media/ryhunsaker/250GB/GitHubRepos/StudentGUI_java/src/main/java/com/studentgui/apphelpers/dto/SessionPayload.java=1761599580000 +/run/media/ryhunsaker/250GB/GitHubRepos/StudentGUI_java/src/main/resources/version.properties=1760971182000 +configuration*?=15848656DE3CEF8359A6E751071AE97430CFD71F +module-resource*?\:checkstyle-suppressions.xml=4E65BAA393FEB25D3F7750BF603CA142D71188C2 +module-resource*?\:checkstyle-xpath-suppressions.xml=7C926BB3C8AF302182A2FDAF330F2434B6DA53A diff --git a/target/checkstyle-checker.xml b/target/checkstyle-checker.xml new file mode 100644 index 0000000..eed537b --- /dev/null +++ b/target/checkstyle-checker.xml @@ -0,0 +1,198 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/target/checkstyle-result.xml b/target/checkstyle-result.xml new file mode 100644 index 0000000..e954625 --- /dev/null +++ b/target/checkstyle-result.xml @@ -0,0 +1,3197 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/target/classes/.gitkeep b/target/classes/.gitkeep deleted file mode 100644 index 78676ae..0000000 --- a/target/classes/.gitkeep +++ /dev/null @@ -1,3 +0,0 @@ -# .gitkeep placeholder to avoid committing empty directories -# Root-level placeholder files (e.g., Abacus.java, Braille.java) were removed -# Packaged classes live under `com/studentgui/apppages` and are the canonical sources. diff --git a/target/classes/logback.xml b/target/classes/logback.xml index ec11e1f..32d8476 100644 --- a/target/classes/logback.xml +++ b/target/classes/logback.xml @@ -1,50 +1,50 @@ - - - - - - - - - - - - - - - - ${PATTERN} - - - - - - ${APP_HOME}/logs/log_${LOG_TS}.log - - - ${APP_HOME}/logs/log_${LOG_TS}.%d{yyyy-MM-dd}.%i.log - 10MB - 30 - 1GB - - - ${PATTERN} - - - - - - - - - - - - - - + + + + + + + + + + + + + + + + ${PATTERN} + + + + + + ${APP_HOME}/logs/log_${LOG_TS}.log + + + ${APP_HOME}/logs/log_${LOG_TS}.%d{yyyy-MM-dd}.%i.log + 10MB + 30 + 1GB + + + ${PATTERN} + + + + + + + + + + + + + + diff --git a/target/classes/version.properties b/target/classes/version.properties index 000152e..18e2cca 100644 --- a/target/classes/version.properties +++ b/target/classes/version.properties @@ -1 +1 @@ -version=1.0.0-beta +version=1.0.0-beta diff --git a/target/javadoc-bundle-options/javadoc-options-javadoc-resources.xml b/target/javadoc-bundle-options/javadoc-options-javadoc-resources.xml index 8b89c97..092103a 100644 --- a/target/javadoc-bundle-options/javadoc-options-javadoc-resources.xml +++ b/target/javadoc-bundle-options/javadoc-options-javadoc-resources.xml @@ -6,5 +6,8 @@ - src/main/javadoc + + ../apidocs + + src/test/javadoc diff --git a/target/maven-archiver/pom.properties b/target/maven-archiver/pom.properties deleted file mode 100644 index d10f1f3..0000000 --- a/target/maven-archiver/pom.properties +++ /dev/null @@ -1,3 +0,0 @@ -artifactId=Main -groupId=com.example -version=1.0.0-beta diff --git a/target/maven-javadoc-plugin-stale-data.txt b/target/maven-javadoc-plugin-stale-data.txt index 5a36655..092f784 100644 --- a/target/maven-javadoc-plugin-stale-data.txt +++ b/target/maven-javadoc-plugin-stale-data.txt @@ -2,101 +2,57 @@ -J-Duser.country= @options @packages -@argfile --classpath -'C:/Users/Ryan Hunsaker/.m2/repository/org/jfree/jfreechart/1.5.5/jfreechart-1.5.5.jar;C:/Users/Ryan Hunsaker/.m2/repository/org/xerial/sqlite-jdbc/3.46.0.1/sqlite-jdbc-3.46.0.1.jar;C:/Users/Ryan Hunsaker/.m2/repository/com/itextpdf/itextpdf/5.5.13.4/itextpdf-5.5.13.4.jar;C:/Users/Ryan Hunsaker/.m2/repository/com/formdev/flatlaf/3.5.1/flatlaf-3.5.1.jar;C:/Users/Ryan Hunsaker/.m2/repository/com/formdev/flatlaf-intellij-themes/3.5.1/flatlaf-intellij-themes-3.5.1.jar;C:/Users/Ryan Hunsaker/.m2/repository/org/slf4j/slf4j-api/2.0.16/slf4j-api-2.0.16.jar;C:/Users/Ryan Hunsaker/.m2/repository/com/fasterxml/jackson/core/jackson-databind/2.15.2/jackson-databind-2.15.2.jar;C:/Users/Ryan Hunsaker/.m2/repository/com/fasterxml/jackson/core/jackson-annotations/2.15.2/jackson-annotations-2.15.2.jar;C:/Users/Ryan Hunsaker/.m2/repository/com/fasterxml/jackson/core/jackson-core/2.15.2/jackson-core-2.15.2.jar;C:/Users/Ryan Hunsaker/.m2/repository/ch/qos/logback/logback-classic/1.5.7/logback-classic-1.5.7.jar;C:/Users/Ryan Hunsaker/.m2/repository/ch/qos/logback/logback-core/1.5.7/logback-core-1.5.7.jar' +--class-path +'/run/media/ryhunsaker/250GB/GitHubRepos/StudentGUI_java/target/classes:/run/media/ryhunsaker/250GB/GitHubRepos/StudentGUI_java/target/test-classes:/var/home/ryhunsaker/.m2/repository/org/jfree/jfreechart/1.5.5/jfreechart-1.5.5.jar:/var/home/ryhunsaker/.m2/repository/org/xerial/sqlite-jdbc/3.46.0.1/sqlite-jdbc-3.46.0.1.jar:/var/home/ryhunsaker/.m2/repository/com/itextpdf/itextpdf/5.5.13.4/itextpdf-5.5.13.4.jar:/var/home/ryhunsaker/.m2/repository/com/formdev/flatlaf/3.5.1/flatlaf-3.5.1.jar:/var/home/ryhunsaker/.m2/repository/com/formdev/flatlaf-intellij-themes/3.5.1/flatlaf-intellij-themes-3.5.1.jar:/var/home/ryhunsaker/.m2/repository/org/slf4j/slf4j-api/2.0.16/slf4j-api-2.0.16.jar:/var/home/ryhunsaker/.m2/repository/com/fasterxml/jackson/core/jackson-databind/2.15.2/jackson-databind-2.15.2.jar:/var/home/ryhunsaker/.m2/repository/com/fasterxml/jackson/core/jackson-annotations/2.15.2/jackson-annotations-2.15.2.jar:/var/home/ryhunsaker/.m2/repository/com/fasterxml/jackson/core/jackson-core/2.15.2/jackson-core-2.15.2.jar:/var/home/ryhunsaker/.m2/repository/ch/qos/logback/logback-classic/1.5.7/logback-classic-1.5.7.jar:/var/home/ryhunsaker/.m2/repository/ch/qos/logback/logback-core/1.5.7/logback-core-1.5.7.jar:/var/home/ryhunsaker/.m2/repository/org/junit/jupiter/junit-jupiter/5.10.0/junit-jupiter-5.10.0.jar:/var/home/ryhunsaker/.m2/repository/org/junit/jupiter/junit-jupiter-api/5.10.0/junit-jupiter-api-5.10.0.jar:/var/home/ryhunsaker/.m2/repository/org/opentest4j/opentest4j/1.3.0/opentest4j-1.3.0.jar:/var/home/ryhunsaker/.m2/repository/org/junit/platform/junit-platform-commons/1.10.0/junit-platform-commons-1.10.0.jar:/var/home/ryhunsaker/.m2/repository/org/apiguardian/apiguardian-api/1.1.2/apiguardian-api-1.1.2.jar:/var/home/ryhunsaker/.m2/repository/org/junit/jupiter/junit-jupiter-params/5.10.0/junit-jupiter-params-5.10.0.jar:/var/home/ryhunsaker/.m2/repository/org/junit/jupiter/junit-jupiter-engine/5.10.0/junit-jupiter-engine-5.10.0.jar:/var/home/ryhunsaker/.m2/repository/org/junit/platform/junit-platform-engine/1.10.0/junit-platform-engine-1.10.0.jar' -encoding 'UTF-8' -protected +-quiet --release 21 -sourcepath -'D:/GitHubRepos/StudentGUI_java/src/main/java;D:/GitHubRepos/StudentGUI_java/target/generated-sources/annotations' +'/run/media/ryhunsaker/250GB/GitHubRepos/StudentGUI_java/src/test/java:/run/media/ryhunsaker/250GB/GitHubRepos/StudentGUI_java/target/generated-test-sources/test-annotations' -author -bottom 'Copyright © 2025 Michael Ryan Hunsaker, M.Ed., Ph.D.. All rights reserved.' -charset 'UTF-8' -d -'D:/GitHubRepos/StudentGUI_java/target/apidocs' +'/run/media/ryhunsaker/250GB/GitHubRepos/StudentGUI_java/target/site/testapidocs' -docencoding 'UTF-8' +-Xdoclint:none -doctitle -'Vision Skills Progression Tracker 1.0.0-beta API' +'Vision Skills Progression Tracker 1.0.0-beta Test API' +-link +'../apidocs' +-linksource -use -version -windowtitle -'Vision Skills Progression Tracker 1.0.0-beta API' +'Vision Skills Progression Tracker 1.0.0-beta Test API' +com.studentgui.test com.studentgui.apphelpers -com.studentgui.apphelpers.dto -com.studentgui.app -com.studentgui.apptheming com.studentgui.apppages -com.studentgui.test -com.studentgui.uicomp -com.studentgui.tools -com.studentgui.bootstrap -'D:/GitHubRepos/StudentGUI_java/src/main/java/Abacus.java' -'D:/GitHubRepos/StudentGUI_java/src/main/java/Braille.java' -'D:/GitHubRepos/StudentGUI_java/src/main/java/BrailleNote.java' -'D:/GitHubRepos/StudentGUI_java/src/main/java/BrailleSense.java' -'D:/GitHubRepos/StudentGUI_java/src/main/java/CVI.java' -'D:/GitHubRepos/StudentGUI_java/src/main/java/DigitalLiteracy.java' -'D:/GitHubRepos/StudentGUI_java/src/main/java/IOS.java' -'D:/GitHubRepos/StudentGUI_java/src/main/java/JLineGraph.java' -'D:/GitHubRepos/StudentGUI_java/src/main/java/Keyboarding.java' -'D:/GitHubRepos/StudentGUI_java/src/main/java/Main.java' -'D:/GitHubRepos/StudentGUI_java/src/main/java/ScreenReader.java' -'D:/GitHubRepos/StudentGUI_java/src/main/java/VersionUtil.java' -C:\Users\Ryan Hunsaker\.m2\repository\org\jfree\jfreechart\1.5.5\jfreechart-1.5.5.jar = 1760971282085 -C:\Users\Ryan Hunsaker\.m2\repository\org\xerial\sqlite-jdbc\3.46.0.1\sqlite-jdbc-3.46.0.1.jar = 1760971282951 -C:\Users\Ryan Hunsaker\.m2\repository\com\itextpdf\itextpdf\5.5.13.4\itextpdf-5.5.13.4.jar = 1757077267990 -C:\Users\Ryan Hunsaker\.m2\repository\com\formdev\flatlaf\3.5.1\flatlaf-3.5.1.jar = 1757077267310 -C:\Users\Ryan Hunsaker\.m2\repository\com\formdev\flatlaf-intellij-themes\3.5.1\flatlaf-intellij-themes-3.5.1.jar = 1757077266919 -C:\Users\Ryan Hunsaker\.m2\repository\org\slf4j\slf4j-api\2.0.16\slf4j-api-2.0.16.jar = 1757077266824 -C:\Users\Ryan Hunsaker\.m2\repository\com\fasterxml\jackson\core\jackson-databind\2.15.2\jackson-databind-2.15.2.jar = 1761150382661 -C:\Users\Ryan Hunsaker\.m2\repository\com\fasterxml\jackson\core\jackson-annotations\2.15.2\jackson-annotations-2.15.2.jar = 1761150382751 -C:\Users\Ryan Hunsaker\.m2\repository\com\fasterxml\jackson\core\jackson-core\2.15.2\jackson-core-2.15.2.jar = 1761150382845 -C:\Users\Ryan Hunsaker\.m2\repository\ch\qos\logback\logback-classic\1.5.7\logback-classic-1.5.7.jar = 1757077266886 -C:\Users\Ryan Hunsaker\.m2\repository\ch\qos\logback\logback-core\1.5.7\logback-core-1.5.7.jar = 1757077267348 -D:\GitHubRepos\StudentGUI_java\src\main\java\Abacus.java = 1761320004000 -D:\GitHubRepos\StudentGUI_java\src\main\java\Braille.java = 1761320004000 -D:\GitHubRepos\StudentGUI_java\src\main\java\BrailleNote.java = 1761320004000 -D:\GitHubRepos\StudentGUI_java\src\main\java\BrailleSense.java = 1761320004000 -D:\GitHubRepos\StudentGUI_java\src\main\java\CVI.java = 1761320004000 -D:\GitHubRepos\StudentGUI_java\src\main\java\DigitalLiteracy.java = 1761320004000 -D:\GitHubRepos\StudentGUI_java\src\main\java\IOS.java = 1761320004000 -D:\GitHubRepos\StudentGUI_java\src\main\java\JLineGraph.java = 1760975390000 -D:\GitHubRepos\StudentGUI_java\src\main\java\Keyboarding.java = 1761320004000 -D:\GitHubRepos\StudentGUI_java\src\main\java\Main.java = 1760973420000 -D:\GitHubRepos\StudentGUI_java\src\main\java\ScreenReader.java = 1761320004000 -D:\GitHubRepos\StudentGUI_java\src\main\java\VersionUtil.java = 1761058032000 -D:\GitHubRepos\StudentGUI_java\src\main\java\.gitkeep = 1761318064000 -D:\GitHubRepos\StudentGUI_java\src\main\java = 1760971181260 -D:\GitHubRepos\StudentGUI_java\target\generated-sources\annotations = 1762192804790 -D:\GitHubRepos\StudentGUI_java\target\apidocs\VersionUtil.html = 1762192814000 -D:\GitHubRepos\StudentGUI_java\target\apidocs\element-list = 1762192814000 -D:\GitHubRepos\StudentGUI_java\target\apidocs\package-summary.html = 1762192814000 -D:\GitHubRepos\StudentGUI_java\target\apidocs\package-tree.html = 1762192814000 -D:\GitHubRepos\StudentGUI_java\target\apidocs\serialized-form.html = 1762192814000 -D:\GitHubRepos\StudentGUI_java\target\apidocs\package-use.html = 1762192816000 -D:\GitHubRepos\StudentGUI_java\target\apidocs\overview-tree.html = 1762192816000 -D:\GitHubRepos\StudentGUI_java\target\apidocs\deprecated-list.html = 1762192816000 -D:\GitHubRepos\StudentGUI_java\target\apidocs\index.html = 1762192816000 -D:\GitHubRepos\StudentGUI_java\target\apidocs\allclasses-index.html = 1762192816000 -D:\GitHubRepos\StudentGUI_java\target\apidocs\allpackages-index.html = 1762192816000 -D:\GitHubRepos\StudentGUI_java\target\apidocs\module-search-index.js = 1762192816000 -D:\GitHubRepos\StudentGUI_java\target\apidocs\package-search-index.js = 1762192816000 -D:\GitHubRepos\StudentGUI_java\target\apidocs\type-search-index.js = 1762192816000 -D:\GitHubRepos\StudentGUI_java\target\apidocs\member-search-index.js = 1762192818000 -D:\GitHubRepos\StudentGUI_java\target\apidocs\tag-search-index.js = 1762192818000 -D:\GitHubRepos\StudentGUI_java\target\apidocs\index-all.html = 1762192818000 -D:\GitHubRepos\StudentGUI_java\target\apidocs\search.html = 1762192818000 -D:\GitHubRepos\StudentGUI_java\target\apidocs\overview-summary.html = 1762192818000 -D:\GitHubRepos\StudentGUI_java\target\apidocs\help-doc.html = 1762192818000 -D:\GitHubRepos\StudentGUI_java\target\apidocs\stylesheet.css = 1762192818000 -D:\GitHubRepos\StudentGUI_java\target\apidocs\script.js = 1762192818000 -D:\GitHubRepos\StudentGUI_java\target\apidocs\copy.svg = 1762192818000 -D:\GitHubRepos\StudentGUI_java\target\apidocs\link.svg = 1762192818000 -D:\GitHubRepos\StudentGUI_java\target\apidocs\search.js = 1762192818000 -D:\GitHubRepos\StudentGUI_java\target\apidocs\search-page.js = 1762192818000 +/run/media/ryhunsaker/250GB/GitHubRepos/StudentGUI_java/src/test/java = 1760994029890 +/run/media/ryhunsaker/250GB/GitHubRepos/StudentGUI_java/target/generated-test-sources/test-annotations = 1766000669760 +/run/media/ryhunsaker/250GB/GitHubRepos/StudentGUI_java/target/site/testapidocs/element-list = 1766000679030 +/run/media/ryhunsaker/250GB/GitHubRepos/StudentGUI_java/target/site/testapidocs/overview-tree.html = 1766000679060 +/run/media/ryhunsaker/250GB/GitHubRepos/StudentGUI_java/target/site/testapidocs/index.html = 1766000679070 +/run/media/ryhunsaker/250GB/GitHubRepos/StudentGUI_java/target/site/testapidocs/allclasses-index.html = 1766000679070 +/run/media/ryhunsaker/250GB/GitHubRepos/StudentGUI_java/target/site/testapidocs/allpackages-index.html = 1766000679080 +/run/media/ryhunsaker/250GB/GitHubRepos/StudentGUI_java/target/site/testapidocs/module-search-index.js = 1766000679080 +/run/media/ryhunsaker/250GB/GitHubRepos/StudentGUI_java/target/site/testapidocs/package-search-index.js = 1766000679080 +/run/media/ryhunsaker/250GB/GitHubRepos/StudentGUI_java/target/site/testapidocs/type-search-index.js = 1766000679080 +/run/media/ryhunsaker/250GB/GitHubRepos/StudentGUI_java/target/site/testapidocs/member-search-index.js = 1766000679080 +/run/media/ryhunsaker/250GB/GitHubRepos/StudentGUI_java/target/site/testapidocs/tag-search-index.js = 1766000679080 +/run/media/ryhunsaker/250GB/GitHubRepos/StudentGUI_java/target/site/testapidocs/index-all.html = 1766000679080 +/run/media/ryhunsaker/250GB/GitHubRepos/StudentGUI_java/target/site/testapidocs/search.html = 1766000679090 +/run/media/ryhunsaker/250GB/GitHubRepos/StudentGUI_java/target/site/testapidocs/overview-summary.html = 1766000679090 +/run/media/ryhunsaker/250GB/GitHubRepos/StudentGUI_java/target/site/testapidocs/help-doc.html = 1766000679090 +/run/media/ryhunsaker/250GB/GitHubRepos/StudentGUI_java/target/site/testapidocs/stylesheet.css = 1766000679090 +/run/media/ryhunsaker/250GB/GitHubRepos/StudentGUI_java/target/site/testapidocs/script.js = 1766000679090 +/run/media/ryhunsaker/250GB/GitHubRepos/StudentGUI_java/target/site/testapidocs/copy.svg = 1766000679090 +/run/media/ryhunsaker/250GB/GitHubRepos/StudentGUI_java/target/site/testapidocs/link.svg = 1766000679090 +/run/media/ryhunsaker/250GB/GitHubRepos/StudentGUI_java/target/site/testapidocs/search.js = 1766000679090 +/run/media/ryhunsaker/250GB/GitHubRepos/StudentGUI_java/target/site/testapidocs/search-page.js = 1766000679090 diff --git a/target/maven-status/maven-compiler-plugin/compile/default-compile/createdFiles.lst b/target/maven-status/maven-compiler-plugin/compile/default-compile/createdFiles.lst index a92e896..839c4cd 100644 --- a/target/maven-status/maven-compiler-plugin/compile/default-compile/createdFiles.lst +++ b/target/maven-status/maven-compiler-plugin/compile/default-compile/createdFiles.lst @@ -1,61 +1,58 @@ -com\studentgui\apppages\ScreenReader.class -com\studentgui\apptheming\Theme.class -com\studentgui\apppages\BrailleNote.class -com\studentgui\apphelpers\Settings.class -com\studentgui\tools\RenderStudentProgress.class -com\studentgui\apptheming\Theme$14.class -com\studentgui\apppages\SessionNotes.class -com\studentgui\apphelpers\SessionJsonWriter.class -com\studentgui\apptheming\Theme$6.class -com\studentgui\app\PreferencesDialog.class -com\studentgui\uicomp\PhaseScoreField.class -com\studentgui\apppages\JLineGraph$1.class -com\studentgui\tools\GroupedSmoke.class -com\studentgui\app\Main.class -com\studentgui\apptheming\Theme$3.class +com/studentgui/apptheming/Theme$8.class +com/studentgui/uicomp/PhaseScoreField.class +com/studentgui/apptheming/Theme$12.class +com/studentgui/apptheming/Theme$16.class +com/studentgui/tools/SmokeTest.class +com/studentgui/apppages/BrailleNote.class +com/studentgui/apppages/JLineGraph.class +com/studentgui/apptheming/Theme$1.class +com/studentgui/apphelpers/dto/AssessmentPayload.class +com/studentgui/apphelpers/dto/KeyboardingPayload.class +com/studentgui/apppages/JLineGraph$1.class PlaceholderMain.class -com\studentgui\apptheming\Theme$10.class -com\studentgui\apppages\Observations.class -com\studentgui\apphelpers\dto\ContactPayload.class -com\studentgui\apptheming\Theme$13.class -com\studentgui\test\BrailleSmokeTest.class -com\studentgui\tools\QueryStudentData.class -com\studentgui\apppages\Homepage.class -com\studentgui\apphelpers\SqlGenerate.class -com\studentgui\apphelpers\UiNotifier.class -com\studentgui\apphelpers\dto\SessionPayload.class -com\studentgui\apptheming\Theme$7.class -com\studentgui\app\StudentChangeListener.class -com\studentgui\apptheming\Theme$4.class -com\studentgui\app\SettingsChangeListener.class -com\studentgui\apppages\IOS.class -com\studentgui\tools\SmokeTest.class -com\studentgui\apptheming\Theme$16.class -com\studentgui\apphelpers\PythonPlotter.class -com\studentgui\apppages\CVI.class -com\studentgui\apppages\Keyboarding.class -com\studentgui\tools\ProgrammaticPageSaveTest.class -com\studentgui\apptheming\Theme$1.class -com\studentgui\apppages\Braille.class -com\studentgui\app\DateChangeListener.class -com\studentgui\apptheming\Theme$5.class -com\studentgui\apptheming\Theme$12.class -com\studentgui\apphelpers\dto\KeyboardingPayload.class -com\studentgui\apptheming\Theme$8.class -com\studentgui\apptheming\Theme$2.class -com\studentgui\apptheming\Theme$15.class -com\studentgui\apphelpers\Database$ResultsWithDates.class -com\studentgui\apppages\ContactLog.class -com\studentgui\apphelpers\dto\AssessmentPayload.class -com\studentgui\apphelpers\Database.class -com\studentgui\apptheming\Theme$9.class +com/studentgui/tools/QueryStudentData.class +com/studentgui/apphelpers/dto/SessionPayload.class +com/studentgui/apppages/IOS.class +com/studentgui/apptheming/Theme$4.class +com/studentgui/apphelpers/UiNotifier.class +com/studentgui/apptheming/Theme$15.class +com/studentgui/apppages/Homepage.class +com/studentgui/apphelpers/PythonPlotter.class +com/studentgui/apppages/CVI.class +com/studentgui/app/SettingsChangeListener.class +com/studentgui/tools/GroupedSmoke.class +com/studentgui/apptheming/Theme$7.class +com/studentgui/tools/ProgrammaticPageSaveTest.class +com/studentgui/apppages/Observations.class +com/studentgui/apppages/Abacus.class +com/studentgui/app/DateChangeListener.class +com/studentgui/apphelpers/Database.class +com/studentgui/apphelpers/dto/ContactPayload.class +com/studentgui/apptheming/Theme$18.class +com/studentgui/apptheming/Theme$3.class +com/studentgui/tools/RenderStudentProgress.class +com/studentgui/apptheming/Theme$14.class +com/studentgui/app/StudentChangeListener.class +com/studentgui/apppages/Braille.class +com/studentgui/apphelpers/Settings.class +com/studentgui/apptheming/Theme$6.class +com/studentgui/apptheming/Theme$11.class +com/studentgui/apptheming/Theme$17.class +com/studentgui/apphelpers/SessionJsonWriter.class +com/studentgui/apphelpers/Helpers.class +com/studentgui/apptheming/Theme$9.class +com/studentgui/apppages/Keyboarding.class +com/studentgui/apphelpers/Database$ResultsWithDates.class +com/studentgui/apppages/DigitalLiteracy.class VersionUtil.class -com\studentgui\apptheming\Theme$17.class -com\studentgui\apphelpers\Helpers.class -com\studentgui\apppages\BrailleSense.class -com\studentgui\apphelpers\dto\NotesPayload.class -com\studentgui\apppages\DigitalLiteracy.class -com\studentgui\apppages\Abacus.class -com\studentgui\apppages\InstructionalMaterials.class -com\studentgui\apppages\JLineGraph.class -com\studentgui\apptheming\Theme$11.class +com/studentgui/apppages/SessionNotes.class +com/studentgui/apphelpers/dto/NotesPayload.class +com/studentgui/apptheming/Theme$2.class +com/studentgui/apppages/ContactLog.class +com/studentgui/apppages/ScreenReader.class +com/studentgui/apphelpers/SqlGenerate.class +com/studentgui/test/BrailleSmokeTest.class +com/studentgui/apptheming/Theme$10.class +com/studentgui/apppages/BrailleSense.class +com/studentgui/apptheming/Theme$13.class +com/studentgui/apptheming/Theme$5.class diff --git a/target/maven-status/maven-compiler-plugin/compile/default-compile/inputFiles.lst b/target/maven-status/maven-compiler-plugin/compile/default-compile/inputFiles.lst index 1d97944..0e3336c 100644 --- a/target/maven-status/maven-compiler-plugin/compile/default-compile/inputFiles.lst +++ b/target/maven-status/maven-compiler-plugin/compile/default-compile/inputFiles.lst @@ -1,53 +1,53 @@ -D:\GitHubRepos\StudentGUI_java\src\main\java\Abacus.java -D:\GitHubRepos\StudentGUI_java\src\main\java\Braille.java -D:\GitHubRepos\StudentGUI_java\src\main\java\BrailleNote.java -D:\GitHubRepos\StudentGUI_java\src\main\java\BrailleSense.java -D:\GitHubRepos\StudentGUI_java\src\main\java\com\studentgui\app\DateChangeListener.java -D:\GitHubRepos\StudentGUI_java\src\main\java\com\studentgui\app\Main.java -D:\GitHubRepos\StudentGUI_java\src\main\java\com\studentgui\app\PreferencesDialog.java -D:\GitHubRepos\StudentGUI_java\src\main\java\com\studentgui\app\SettingsChangeListener.java -D:\GitHubRepos\StudentGUI_java\src\main\java\com\studentgui\app\StudentChangeListener.java -D:\GitHubRepos\StudentGUI_java\src\main\java\com\studentgui\apphelpers\Database.java -D:\GitHubRepos\StudentGUI_java\src\main\java\com\studentgui\apphelpers\dto\AssessmentPayload.java -D:\GitHubRepos\StudentGUI_java\src\main\java\com\studentgui\apphelpers\dto\ContactPayload.java -D:\GitHubRepos\StudentGUI_java\src\main\java\com\studentgui\apphelpers\dto\KeyboardingPayload.java -D:\GitHubRepos\StudentGUI_java\src\main\java\com\studentgui\apphelpers\dto\NotesPayload.java -D:\GitHubRepos\StudentGUI_java\src\main\java\com\studentgui\apphelpers\dto\SessionPayload.java -D:\GitHubRepos\StudentGUI_java\src\main\java\com\studentgui\apphelpers\Helpers.java -D:\GitHubRepos\StudentGUI_java\src\main\java\com\studentgui\apphelpers\PythonPlotter.java -D:\GitHubRepos\StudentGUI_java\src\main\java\com\studentgui\apphelpers\SessionJsonWriter.java -D:\GitHubRepos\StudentGUI_java\src\main\java\com\studentgui\apphelpers\Settings.java -D:\GitHubRepos\StudentGUI_java\src\main\java\com\studentgui\apphelpers\SqlGenerate.java -D:\GitHubRepos\StudentGUI_java\src\main\java\com\studentgui\apphelpers\UiNotifier.java -D:\GitHubRepos\StudentGUI_java\src\main\java\com\studentgui\apppages\Abacus.java -D:\GitHubRepos\StudentGUI_java\src\main\java\com\studentgui\apppages\Braille.java -D:\GitHubRepos\StudentGUI_java\src\main\java\com\studentgui\apppages\BrailleNote.java -D:\GitHubRepos\StudentGUI_java\src\main\java\com\studentgui\apppages\BrailleSense.java -D:\GitHubRepos\StudentGUI_java\src\main\java\com\studentgui\apppages\ContactLog.java -D:\GitHubRepos\StudentGUI_java\src\main\java\com\studentgui\apppages\CVI.java -D:\GitHubRepos\StudentGUI_java\src\main\java\com\studentgui\apppages\DigitalLiteracy.java -D:\GitHubRepos\StudentGUI_java\src\main\java\com\studentgui\apppages\Homepage.java -D:\GitHubRepos\StudentGUI_java\src\main\java\com\studentgui\apppages\InstructionalMaterials.java -D:\GitHubRepos\StudentGUI_java\src\main\java\com\studentgui\apppages\IOS.java -D:\GitHubRepos\StudentGUI_java\src\main\java\com\studentgui\apppages\JLineGraph.java -D:\GitHubRepos\StudentGUI_java\src\main\java\com\studentgui\apppages\Keyboarding.java -D:\GitHubRepos\StudentGUI_java\src\main\java\com\studentgui\apppages\Observations.java -D:\GitHubRepos\StudentGUI_java\src\main\java\com\studentgui\apppages\ScreenReader.java -D:\GitHubRepos\StudentGUI_java\src\main\java\com\studentgui\apppages\SessionNotes.java -D:\GitHubRepos\StudentGUI_java\src\main\java\com\studentgui\apptheming\Theme.java -D:\GitHubRepos\StudentGUI_java\src\main\java\com\studentgui\bootstrap\Bootstrap.java -D:\GitHubRepos\StudentGUI_java\src\main\java\com\studentgui\test\BrailleSmokeTest.java -D:\GitHubRepos\StudentGUI_java\src\main\java\com\studentgui\tools\GroupedSmoke.java -D:\GitHubRepos\StudentGUI_java\src\main\java\com\studentgui\tools\ProgrammaticPageSaveTest.java -D:\GitHubRepos\StudentGUI_java\src\main\java\com\studentgui\tools\QueryStudentData.java -D:\GitHubRepos\StudentGUI_java\src\main\java\com\studentgui\tools\RenderStudentProgress.java -D:\GitHubRepos\StudentGUI_java\src\main\java\com\studentgui\tools\SmokeTest.java -D:\GitHubRepos\StudentGUI_java\src\main\java\com\studentgui\uicomp\PhaseScoreField.java -D:\GitHubRepos\StudentGUI_java\src\main\java\CVI.java -D:\GitHubRepos\StudentGUI_java\src\main\java\DigitalLiteracy.java -D:\GitHubRepos\StudentGUI_java\src\main\java\IOS.java -D:\GitHubRepos\StudentGUI_java\src\main\java\JLineGraph.java -D:\GitHubRepos\StudentGUI_java\src\main\java\Keyboarding.java -D:\GitHubRepos\StudentGUI_java\src\main\java\Main.java -D:\GitHubRepos\StudentGUI_java\src\main\java\ScreenReader.java -D:\GitHubRepos\StudentGUI_java\src\main\java\VersionUtil.java +/run/media/ryhunsaker/250GB/GitHubRepos/StudentGUI_java/src/main/java/Abacus.java +/run/media/ryhunsaker/250GB/GitHubRepos/StudentGUI_java/src/main/java/Braille.java +/run/media/ryhunsaker/250GB/GitHubRepos/StudentGUI_java/src/main/java/BrailleNote.java +/run/media/ryhunsaker/250GB/GitHubRepos/StudentGUI_java/src/main/java/BrailleSense.java +/run/media/ryhunsaker/250GB/GitHubRepos/StudentGUI_java/src/main/java/CVI.java +/run/media/ryhunsaker/250GB/GitHubRepos/StudentGUI_java/src/main/java/DigitalLiteracy.java +/run/media/ryhunsaker/250GB/GitHubRepos/StudentGUI_java/src/main/java/IOS.java +/run/media/ryhunsaker/250GB/GitHubRepos/StudentGUI_java/src/main/java/JLineGraph.java +/run/media/ryhunsaker/250GB/GitHubRepos/StudentGUI_java/src/main/java/Keyboarding.java +/run/media/ryhunsaker/250GB/GitHubRepos/StudentGUI_java/src/main/java/Main.java +/run/media/ryhunsaker/250GB/GitHubRepos/StudentGUI_java/src/main/java/ScreenReader.java +/run/media/ryhunsaker/250GB/GitHubRepos/StudentGUI_java/src/main/java/VersionUtil.java +/run/media/ryhunsaker/250GB/GitHubRepos/StudentGUI_java/src/main/java/com/studentgui/app/DateChangeListener.java +/run/media/ryhunsaker/250GB/GitHubRepos/StudentGUI_java/src/main/java/com/studentgui/app/Main.java +/run/media/ryhunsaker/250GB/GitHubRepos/StudentGUI_java/src/main/java/com/studentgui/app/PreferencesDialog.java +/run/media/ryhunsaker/250GB/GitHubRepos/StudentGUI_java/src/main/java/com/studentgui/app/SettingsChangeListener.java +/run/media/ryhunsaker/250GB/GitHubRepos/StudentGUI_java/src/main/java/com/studentgui/app/StudentChangeListener.java +/run/media/ryhunsaker/250GB/GitHubRepos/StudentGUI_java/src/main/java/com/studentgui/apphelpers/Database.java +/run/media/ryhunsaker/250GB/GitHubRepos/StudentGUI_java/src/main/java/com/studentgui/apphelpers/Helpers.java +/run/media/ryhunsaker/250GB/GitHubRepos/StudentGUI_java/src/main/java/com/studentgui/apphelpers/PythonPlotter.java +/run/media/ryhunsaker/250GB/GitHubRepos/StudentGUI_java/src/main/java/com/studentgui/apphelpers/SessionJsonWriter.java +/run/media/ryhunsaker/250GB/GitHubRepos/StudentGUI_java/src/main/java/com/studentgui/apphelpers/Settings.java +/run/media/ryhunsaker/250GB/GitHubRepos/StudentGUI_java/src/main/java/com/studentgui/apphelpers/SqlGenerate.java +/run/media/ryhunsaker/250GB/GitHubRepos/StudentGUI_java/src/main/java/com/studentgui/apphelpers/UiNotifier.java +/run/media/ryhunsaker/250GB/GitHubRepos/StudentGUI_java/src/main/java/com/studentgui/apphelpers/dto/AssessmentPayload.java +/run/media/ryhunsaker/250GB/GitHubRepos/StudentGUI_java/src/main/java/com/studentgui/apphelpers/dto/ContactPayload.java +/run/media/ryhunsaker/250GB/GitHubRepos/StudentGUI_java/src/main/java/com/studentgui/apphelpers/dto/KeyboardingPayload.java +/run/media/ryhunsaker/250GB/GitHubRepos/StudentGUI_java/src/main/java/com/studentgui/apphelpers/dto/NotesPayload.java +/run/media/ryhunsaker/250GB/GitHubRepos/StudentGUI_java/src/main/java/com/studentgui/apphelpers/dto/SessionPayload.java +/run/media/ryhunsaker/250GB/GitHubRepos/StudentGUI_java/src/main/java/com/studentgui/apppages/Abacus.java +/run/media/ryhunsaker/250GB/GitHubRepos/StudentGUI_java/src/main/java/com/studentgui/apppages/Braille.java +/run/media/ryhunsaker/250GB/GitHubRepos/StudentGUI_java/src/main/java/com/studentgui/apppages/BrailleNote.java +/run/media/ryhunsaker/250GB/GitHubRepos/StudentGUI_java/src/main/java/com/studentgui/apppages/BrailleSense.java +/run/media/ryhunsaker/250GB/GitHubRepos/StudentGUI_java/src/main/java/com/studentgui/apppages/CVI.java +/run/media/ryhunsaker/250GB/GitHubRepos/StudentGUI_java/src/main/java/com/studentgui/apppages/ContactLog.java +/run/media/ryhunsaker/250GB/GitHubRepos/StudentGUI_java/src/main/java/com/studentgui/apppages/DigitalLiteracy.java +/run/media/ryhunsaker/250GB/GitHubRepos/StudentGUI_java/src/main/java/com/studentgui/apppages/Homepage.java +/run/media/ryhunsaker/250GB/GitHubRepos/StudentGUI_java/src/main/java/com/studentgui/apppages/IOS.java +/run/media/ryhunsaker/250GB/GitHubRepos/StudentGUI_java/src/main/java/com/studentgui/apppages/InstructionalMaterials.java +/run/media/ryhunsaker/250GB/GitHubRepos/StudentGUI_java/src/main/java/com/studentgui/apppages/JLineGraph.java +/run/media/ryhunsaker/250GB/GitHubRepos/StudentGUI_java/src/main/java/com/studentgui/apppages/Keyboarding.java +/run/media/ryhunsaker/250GB/GitHubRepos/StudentGUI_java/src/main/java/com/studentgui/apppages/Observations.java +/run/media/ryhunsaker/250GB/GitHubRepos/StudentGUI_java/src/main/java/com/studentgui/apppages/ScreenReader.java +/run/media/ryhunsaker/250GB/GitHubRepos/StudentGUI_java/src/main/java/com/studentgui/apppages/SessionNotes.java +/run/media/ryhunsaker/250GB/GitHubRepos/StudentGUI_java/src/main/java/com/studentgui/apptheming/Theme.java +/run/media/ryhunsaker/250GB/GitHubRepos/StudentGUI_java/src/main/java/com/studentgui/bootstrap/Bootstrap.java +/run/media/ryhunsaker/250GB/GitHubRepos/StudentGUI_java/src/main/java/com/studentgui/test/BrailleSmokeTest.java +/run/media/ryhunsaker/250GB/GitHubRepos/StudentGUI_java/src/main/java/com/studentgui/tools/GroupedSmoke.java +/run/media/ryhunsaker/250GB/GitHubRepos/StudentGUI_java/src/main/java/com/studentgui/tools/ProgrammaticPageSaveTest.java +/run/media/ryhunsaker/250GB/GitHubRepos/StudentGUI_java/src/main/java/com/studentgui/tools/QueryStudentData.java +/run/media/ryhunsaker/250GB/GitHubRepos/StudentGUI_java/src/main/java/com/studentgui/tools/RenderStudentProgress.java +/run/media/ryhunsaker/250GB/GitHubRepos/StudentGUI_java/src/main/java/com/studentgui/tools/SmokeTest.java +/run/media/ryhunsaker/250GB/GitHubRepos/StudentGUI_java/src/main/java/com/studentgui/uicomp/PhaseScoreField.java diff --git a/target/maven-status/maven-compiler-plugin/testCompile/default-testCompile/createdFiles.lst b/target/maven-status/maven-compiler-plugin/testCompile/default-testCompile/createdFiles.lst index a53b81a..a77c8fc 100644 --- a/target/maven-status/maven-compiler-plugin/testCompile/default-testCompile/createdFiles.lst +++ b/target/maven-status/maven-compiler-plugin/testCompile/default-testCompile/createdFiles.lst @@ -1,9 +1,9 @@ -com\studentgui\apphelpers\SqlGenerateTest.class -com\studentgui\apphelpers\SessionJsonWriterTest.class -com\studentgui\apppages\JLineGraphDeterministicJitterTest.class -com\studentgui\test\ExportBrailleReportsTest.class -com\studentgui\apphelpers\DatabaseContactLogTest.class -com\studentgui\test\BrailleDatabaseTest.class -com\studentgui\test\DatabaseTest.class -com\studentgui\test\BrailleSmokeTest.class -com\studentgui\test\DatabaseEdgeCasesTest.class +com/studentgui/apphelpers/DatabaseContactLogTest.class +com/studentgui/test/BrailleDatabaseTest.class +com/studentgui/test/BrailleSmokeTest.class +com/studentgui/apppages/JLineGraphDeterministicJitterTest.class +com/studentgui/test/DatabaseEdgeCasesTest.class +com/studentgui/apphelpers/SessionJsonWriterTest.class +com/studentgui/apphelpers/SqlGenerateTest.class +com/studentgui/test/DatabaseTest.class +com/studentgui/test/ExportBrailleReportsTest.class diff --git a/target/maven-status/maven-compiler-plugin/testCompile/default-testCompile/inputFiles.lst b/target/maven-status/maven-compiler-plugin/testCompile/default-testCompile/inputFiles.lst index 66bfb6f..3fb68d0 100644 --- a/target/maven-status/maven-compiler-plugin/testCompile/default-testCompile/inputFiles.lst +++ b/target/maven-status/maven-compiler-plugin/testCompile/default-testCompile/inputFiles.lst @@ -1,9 +1,9 @@ -D:\GitHubRepos\StudentGUI_java\src\test\java\com\studentgui\apphelpers\DatabaseContactLogTest.java -D:\GitHubRepos\StudentGUI_java\src\test\java\com\studentgui\apphelpers\SessionJsonWriterTest.java -D:\GitHubRepos\StudentGUI_java\src\test\java\com\studentgui\apphelpers\SqlGenerateTest.java -D:\GitHubRepos\StudentGUI_java\src\test\java\com\studentgui\apppages\JLineGraphDeterministicJitterTest.java -D:\GitHubRepos\StudentGUI_java\src\test\java\com\studentgui\test\BrailleDatabaseTest.java -D:\GitHubRepos\StudentGUI_java\src\test\java\com\studentgui\test\BrailleSmokeTest.java -D:\GitHubRepos\StudentGUI_java\src\test\java\com\studentgui\test\DatabaseEdgeCasesTest.java -D:\GitHubRepos\StudentGUI_java\src\test\java\com\studentgui\test\DatabaseTest.java -D:\GitHubRepos\StudentGUI_java\src\test\java\com\studentgui\test\ExportBrailleReportsTest.java +/run/media/ryhunsaker/250GB/GitHubRepos/StudentGUI_java/src/test/java/com/studentgui/apphelpers/DatabaseContactLogTest.java +/run/media/ryhunsaker/250GB/GitHubRepos/StudentGUI_java/src/test/java/com/studentgui/apphelpers/SessionJsonWriterTest.java +/run/media/ryhunsaker/250GB/GitHubRepos/StudentGUI_java/src/test/java/com/studentgui/apphelpers/SqlGenerateTest.java +/run/media/ryhunsaker/250GB/GitHubRepos/StudentGUI_java/src/test/java/com/studentgui/apppages/JLineGraphDeterministicJitterTest.java +/run/media/ryhunsaker/250GB/GitHubRepos/StudentGUI_java/src/test/java/com/studentgui/test/BrailleDatabaseTest.java +/run/media/ryhunsaker/250GB/GitHubRepos/StudentGUI_java/src/test/java/com/studentgui/test/BrailleSmokeTest.java +/run/media/ryhunsaker/250GB/GitHubRepos/StudentGUI_java/src/test/java/com/studentgui/test/DatabaseEdgeCasesTest.java +/run/media/ryhunsaker/250GB/GitHubRepos/StudentGUI_java/src/test/java/com/studentgui/test/DatabaseTest.java +/run/media/ryhunsaker/250GB/GitHubRepos/StudentGUI_java/src/test/java/com/studentgui/test/ExportBrailleReportsTest.java diff --git a/target/apidocs/VersionUtil.html b/target/site/apidocs/VersionUtil.html similarity index 96% rename from target/apidocs/VersionUtil.html rename to target/site/apidocs/VersionUtil.html index 38bd99f..c9de459 100644 --- a/target/apidocs/VersionUtil.html +++ b/target/site/apidocs/VersionUtil.html @@ -1,11 +1,11 @@ - + VersionUtil (Vision Skills Progression Tracker 1.0.0-beta API) - + @@ -91,7 +91,7 @@

Class VersionUtil


-
public class VersionUtil +
public class VersionUtil extends Object
Utility to surface project version information. @@ -137,7 +137,7 @@

Method Details

  • getVersion

    -
    public static String getVersion()
    +
    public static String getVersion()
    Returns the version of the application.

    This method provides access to the version information that was loaded from the properties file. diff --git a/target/apidocs/allclasses-index.html b/target/site/apidocs/allclasses-index.html similarity index 90% rename from target/apidocs/allclasses-index.html rename to target/site/apidocs/allclasses-index.html index c9e34b4..e6eb982 100644 --- a/target/apidocs/allclasses-index.html +++ b/target/site/apidocs/allclasses-index.html @@ -1,11 +1,11 @@ - + All Classes and Interfaces (Vision Skills Progression Tracker 1.0.0-beta API) - + @@ -60,7 +60,7 @@

    All Classes and Interfaces<
    Description
    -
    Abacus skills progression UI page.
    +
    Abacus computational skills assessment page.
    @@ -74,15 +74,15 @@

    All Classes and Interfaces<

    -
    Braille skills progression UI page.
    +
    Braille skills progression assessment page.
    -
    Braille note-taking skills progression page.
    +
    HumanWare BrailleNote Touch Plus (BNT+) proficiency assessment page.
    -
    BrailleSense skills progression UI page.
    +
    HIMS BrailleSense productivity device proficiency assessment page.
    Deprecated. @@ -91,7 +91,7 @@

    All Classes and Interfaces<

    -
    Contact log page for storing freeform contact notes for a student.
    +
    Structured parent/guardian contact log with validation and freeform notes.
    @@ -99,7 +99,7 @@

    All Classes and Interfaces<

    -
    Cortical Visual Impairment (CVI) progression page.
    +
    Cortical Visual Impairment (CVI) assessment page.
    @@ -116,12 +116,11 @@

    All Classes and Interfaces<

    -
    Digital literacy skills progression page UI.
    +
    Digital literacy and computer skills assessment page.
    -
    Small command-line helper that renders a sample grouped chart and - writes an output PNG to the app data folder.
    +
    Automated smoke test for grouped chart rendering and multi-panel PNG export.
    @@ -133,20 +132,19 @@

    All Classes and Interfaces<

    -
    Instructional materials viewer panel.
    +
    Instructional materials and resources reference page.
    -
    iOS / iPadOS skills progression page.
    +
    iOS and iPadOS assistive technology proficiency assessment page.
    -
    Lightweight line chart component used across pages to display recent - assessment sessions.
    +
    Reusable JFreeChart-based line chart component for visualizing student assessment progress.
    -
    Keyboarding skills page.
    +
    Touch-typing and keyboarding skills assessment page.
    @@ -163,7 +161,7 @@

    All Classes and Interfaces<

    -
    Observations page for recording freeform observational notes.
    +
    Observational notes page for documenting unstructured student behaviors and progress.
    @@ -177,8 +175,7 @@

    All Classes and Interfaces<

    -
    Programmatically create a Braille page, populate PhaseScoreField values, - and trigger the submit action to verify DB insert and PNG export.
    +
    Automated integration test for programmatic page manipulation and database submission.
    @@ -186,17 +183,15 @@

    All Classes and Interfaces<

    -
    Development utility to inspect available students and their recent - progress session rows.
    +
    Command-line inspection tool for viewing student database contents and schema statistics.
    -
    Command-line tool to render a particular student's progress chart - for a named progress type.
    +
    Command-line utility for offline student progress chart rendering and export.
    -
    ScreenReader skills progression page.
    +
    Screen reader proficiency assessment page for desktop/laptop environments.
    @@ -204,7 +199,7 @@

    All Classes and Interfaces<

    -
    Session notes editor page.
    +
    Freeform session notes editor for general observations and reflections.
    @@ -220,7 +215,7 @@

    All Classes and Interfaces<

    -
    Minimal smoke test to exercise the chart rendering and file export.
    +
    Minimal automated smoke test for chart rendering and PNG export functionality.
    @@ -233,7 +228,7 @@

    All Classes and Interfaces<

    -
    Small theming and menu helper.
    +
    Application theming and menu bar construction utilities.
    diff --git a/target/apidocs/allpackages-index.html b/target/site/apidocs/allpackages-index.html similarity index 97% rename from target/apidocs/allpackages-index.html rename to target/site/apidocs/allpackages-index.html index 706b34c..f447419 100644 --- a/target/apidocs/allpackages-index.html +++ b/target/site/apidocs/allpackages-index.html @@ -1,11 +1,11 @@ - + All Packages (Vision Skills Progression Tracker 1.0.0-beta API) - + diff --git a/target/apidocs/class-use/VersionUtil.html b/target/site/apidocs/class-use/VersionUtil.html similarity index 95% rename from target/apidocs/class-use/VersionUtil.html rename to target/site/apidocs/class-use/VersionUtil.html index 34348b4..c89f8a9 100644 --- a/target/apidocs/class-use/VersionUtil.html +++ b/target/site/apidocs/class-use/VersionUtil.html @@ -1,11 +1,11 @@ - + Uses of Class VersionUtil (Vision Skills Progression Tracker 1.0.0-beta API) - + diff --git a/target/apidocs/com/studentgui/app/DateChangeListener.html b/target/site/apidocs/com/studentgui/app/DateChangeListener.html similarity index 92% rename from target/apidocs/com/studentgui/app/DateChangeListener.html rename to target/site/apidocs/com/studentgui/app/DateChangeListener.html index 73ffb3e..c884e04 100644 --- a/target/apidocs/com/studentgui/app/DateChangeListener.html +++ b/target/site/apidocs/com/studentgui/app/DateChangeListener.html @@ -1,11 +1,11 @@ - + DateChangeListener (Vision Skills Progression Tracker 1.0.0-beta API) - + @@ -93,7 +93,7 @@

    Interface DateChangeListe
    Abacus, Braille, BrailleNote, DigitalLiteracy, Keyboarding, ScreenReader

    -
    public interface DateChangeListener
    +
    public interface DateChangeListener
    Simple listener interface for pages that want to be notified when the application-wide selected date changes via the top-bar Apply action.

    @@ -132,7 +132,7 @@

    Method Details

  • dateChanged

    -
    void dateChanged(LocalDate newDate)
    +
    void dateChanged(LocalDate newDate)
    Called when the application date has been changed by the user.
    Parameters:
    diff --git a/target/apidocs/com/studentgui/app/Main.html b/target/site/apidocs/com/studentgui/app/Main.html similarity index 87% rename from target/apidocs/com/studentgui/app/Main.html rename to target/site/apidocs/com/studentgui/app/Main.html index 581611c..e81e2bd 100644 --- a/target/apidocs/com/studentgui/app/Main.html +++ b/target/site/apidocs/com/studentgui/app/Main.html @@ -1,11 +1,11 @@ - + Main (Vision Skills Progression Tracker 1.0.0-beta API) - + @@ -92,7 +92,7 @@

    Class Main


  • -
    public class Main +
    public class Main extends Object
    Application entry point and top-level UI wiring for the Student Skills Progressions application. Builds the main frame, menu and registers per-page @@ -197,7 +197,7 @@

    Method Details

  • addDateChangeListener

    -
    public static void addDateChangeListener(DateChangeListener l)
    +
    public static void addDateChangeListener(DateChangeListener l)
    Register a listener to be notified when the application date is changed via the top bar.
    Parameters:
    @@ -208,7 +208,7 @@

    addDateChangeListener

  • removeDateChangeListener

    -
    public static void removeDateChangeListener(DateChangeListener l)
    +
    Remove a previously registered date change listener.
    Parameters:
    @@ -219,14 +219,14 @@

    removeDateChangeListener

  • clearDateChangeListeners

    -
    public static void clearDateChangeListeners()
    +
    public static void clearDateChangeListeners()
    Clear all registered date change listeners.
  • addStudentChangeListener

    -
    public static void addStudentChangeListener(StudentChangeListener l)
    +
    Register a listener to be notified when the selected student is changed.
    Parameters:
    @@ -237,7 +237,7 @@

    addStudentChangeListener

  • removeStudentChangeListener

    -
    public static void removeStudentChangeListener(StudentChangeListener l)
    +
    Remove a previously registered student change listener.
    Parameters:
    @@ -248,14 +248,14 @@

    removeStudentChangeListener

  • clearStudentChangeListeners

    -
    public static void clearStudentChangeListeners()
    +
    public static void clearStudentChangeListeners()
    Clear all registered student change listeners.
  • addSettingsChangeListener

    -
    public static void addSettingsChangeListener(SettingsChangeListener l)
    +
    Register a listener to be notified when application settings change. Implementations should read values from Settings when SettingsChangeListener.settingsChanged() is invoked.
    @@ -268,7 +268,7 @@

    addSettingsChangeListener

  • removeSettingsChangeListener

    -
    public static void removeSettingsChangeListener(SettingsChangeListener l)
    +
    Remove a previously registered settings change listener.
    Parameters:
    @@ -279,14 +279,14 @@

    removeSettingsChangeListener

  • clearSettingsChangeListeners

    -
    public static void clearSettingsChangeListeners()
    +
    public static void clearSettingsChangeListeners()
    Clear all registered settings change listeners.
  • notifySettingsChanged

    -
    public static void notifySettingsChanged()
    +
    public static void notifySettingsChanged()
    Notify all registered settings listeners that application settings have been changed. This is typically invoked after persisting preferences through Settings.
    @@ -295,7 +295,7 @@

    notifySettingsChanged

  • main

    -
    public static void main(String[] args)
    +
    public static void main(String[] args)
    Application entry point. Initializes helpers, database, and launches the Swing UI on the EDT.
    @@ -307,7 +307,7 @@

    main

  • setTheme

    -
    public static void setTheme(String theme)
    +
    public static void setTheme(String theme)
    Change application theme at runtime. Supported values: "light", "dark", "darcula". This method updates the installed Look and Feel and refreshes the main frame.
    @@ -319,7 +319,7 @@

    setTheme

  • showPage

    -
    public static void showPage(String name, +
    public static void showPage(String name, JComponent comp)
    Show a page previously registered with the CardLayout. If a component is provided and not yet added it will be registered under the given name.
    diff --git a/target/apidocs/com/studentgui/app/PreferencesDialog.html b/target/site/apidocs/com/studentgui/app/PreferencesDialog.html similarity index 94% rename from target/apidocs/com/studentgui/app/PreferencesDialog.html rename to target/site/apidocs/com/studentgui/app/PreferencesDialog.html index c84a5c3..a24d3ca 100644 --- a/target/apidocs/com/studentgui/app/PreferencesDialog.html +++ b/target/site/apidocs/com/studentgui/app/PreferencesDialog.html @@ -1,11 +1,11 @@ - + PreferencesDialog (Vision Skills Progression Tracker 1.0.0-beta API) - + @@ -92,7 +92,7 @@

    Class PreferencesDialog


    -
    public final class PreferencesDialog +
    public final class PreferencesDialog extends Object
    Simple modal preferences dialog exposing a few runtime toggles that affect chart rendering. Preferences are persisted via @@ -137,7 +137,7 @@

    Method Details

  • showDialog

    -
    public static void showDialog(Frame owner)
    +
    public static void showDialog(Frame owner)
    Show the modal preferences dialog. The dialog persists changes to Settings and notifies runtime listeners via Main.notifySettingsChanged().
    diff --git a/target/apidocs/com/studentgui/app/SettingsChangeListener.html b/target/site/apidocs/com/studentgui/app/SettingsChangeListener.html similarity index 94% rename from target/apidocs/com/studentgui/app/SettingsChangeListener.html rename to target/site/apidocs/com/studentgui/app/SettingsChangeListener.html index 2dcc2d5..2b95da2 100644 --- a/target/apidocs/com/studentgui/app/SettingsChangeListener.html +++ b/target/site/apidocs/com/studentgui/app/SettingsChangeListener.html @@ -1,11 +1,11 @@ - + SettingsChangeListener (Vision Skills Progression Tracker 1.0.0-beta API) - + @@ -93,7 +93,7 @@

    Interface SettingsCha
    JLineGraph


  • -
    public interface SettingsChangeListener
    +
    public interface SettingsChangeListener
    Simple listener interface for application-wide settings changes.
    @@ -131,7 +131,7 @@

    Method Details

  • settingsChanged

    -
    void settingsChanged()
    +
    Invoked when application settings have been changed and persisted. Implementations should read the desired values from the Settings helper and update any runtime state accordingly.
    diff --git a/target/apidocs/com/studentgui/app/StudentChangeListener.html b/target/site/apidocs/com/studentgui/app/StudentChangeListener.html similarity index 92% rename from target/apidocs/com/studentgui/app/StudentChangeListener.html rename to target/site/apidocs/com/studentgui/app/StudentChangeListener.html index 6dc74f9..e3cab65 100644 --- a/target/apidocs/com/studentgui/app/StudentChangeListener.html +++ b/target/site/apidocs/com/studentgui/app/StudentChangeListener.html @@ -1,11 +1,11 @@ - + StudentChangeListener (Vision Skills Progression Tracker 1.0.0-beta API) - + @@ -93,7 +93,7 @@

    Interface StudentChang
    Abacus, Braille, BrailleNote, DigitalLiteracy, Keyboarding, ScreenReader


  • -
    public interface StudentChangeListener
    +
    public interface StudentChangeListener
    Listener for application-wide student selection changes.
    @@ -131,7 +131,7 @@

    Method Details

  • studentChanged

    -
    void studentChanged(String newStudent)
    +
    void studentChanged(String newStudent)
    Called when the application selected student has changed.
    Parameters:
    diff --git a/target/apidocs/com/studentgui/app/class-use/DateChangeListener.html b/target/site/apidocs/com/studentgui/app/class-use/DateChangeListener.html similarity index 93% rename from target/apidocs/com/studentgui/app/class-use/DateChangeListener.html rename to target/site/apidocs/com/studentgui/app/class-use/DateChangeListener.html index fe4eeb2..a276a00 100644 --- a/target/apidocs/com/studentgui/app/class-use/DateChangeListener.html +++ b/target/site/apidocs/com/studentgui/app/class-use/DateChangeListener.html @@ -1,11 +1,11 @@ - + Uses of Interface com.studentgui.app.DateChangeListener (Vision Skills Progression Tracker 1.0.0-beta API) - + @@ -95,32 +95,32 @@

    Uses of class 

  • -
    Abacus skills progression UI page.
    +
    Abacus computational skills assessment page.
    class 
    -
    Braille skills progression UI page.
    +
    Braille skills progression assessment page.
    class 
    -
    Braille note-taking skills progression page.
    +
    HumanWare BrailleNote Touch Plus (BNT+) proficiency assessment page.
    class 
    -
    Digital literacy skills progression page UI.
    +
    Digital literacy and computer skills assessment page.
    class 
    -
    Keyboarding skills page.
    +
    Touch-typing and keyboarding skills assessment page.
    class 
    -
    ScreenReader skills progression page.
    +
    Screen reader proficiency assessment page for desktop/laptop environments.
    diff --git a/target/apidocs/com/studentgui/app/class-use/Main.html b/target/site/apidocs/com/studentgui/app/class-use/Main.html similarity index 96% rename from target/apidocs/com/studentgui/app/class-use/Main.html rename to target/site/apidocs/com/studentgui/app/class-use/Main.html index cd94426..39eab4d 100644 --- a/target/apidocs/com/studentgui/app/class-use/Main.html +++ b/target/site/apidocs/com/studentgui/app/class-use/Main.html @@ -1,11 +1,11 @@ - + Uses of Class com.studentgui.app.Main (Vision Skills Progression Tracker 1.0.0-beta API) - + diff --git a/target/apidocs/com/studentgui/app/class-use/PreferencesDialog.html b/target/site/apidocs/com/studentgui/app/class-use/PreferencesDialog.html similarity index 96% rename from target/apidocs/com/studentgui/app/class-use/PreferencesDialog.html rename to target/site/apidocs/com/studentgui/app/class-use/PreferencesDialog.html index 2a82c90..f43159f 100644 --- a/target/apidocs/com/studentgui/app/class-use/PreferencesDialog.html +++ b/target/site/apidocs/com/studentgui/app/class-use/PreferencesDialog.html @@ -1,11 +1,11 @@ - + Uses of Class com.studentgui.app.PreferencesDialog (Vision Skills Progression Tracker 1.0.0-beta API) - + diff --git a/target/apidocs/com/studentgui/app/class-use/SettingsChangeListener.html b/target/site/apidocs/com/studentgui/app/class-use/SettingsChangeListener.html similarity index 96% rename from target/apidocs/com/studentgui/app/class-use/SettingsChangeListener.html rename to target/site/apidocs/com/studentgui/app/class-use/SettingsChangeListener.html index ad485d7..a374a81 100644 --- a/target/apidocs/com/studentgui/app/class-use/SettingsChangeListener.html +++ b/target/site/apidocs/com/studentgui/app/class-use/SettingsChangeListener.html @@ -1,11 +1,11 @@ - + Uses of Interface com.studentgui.app.SettingsChangeListener (Vision Skills Progression Tracker 1.0.0-beta API) - + @@ -95,8 +95,7 @@

    Uses of class 

    -
    Lightweight line chart component used across pages to display recent - assessment sessions.
    +
    Reusable JFreeChart-based line chart component for visualizing student assessment progress.
    diff --git a/target/apidocs/com/studentgui/app/class-use/StudentChangeListener.html b/target/site/apidocs/com/studentgui/app/class-use/StudentChangeListener.html similarity index 93% rename from target/apidocs/com/studentgui/app/class-use/StudentChangeListener.html rename to target/site/apidocs/com/studentgui/app/class-use/StudentChangeListener.html index fe989e9..15a4048 100644 --- a/target/apidocs/com/studentgui/app/class-use/StudentChangeListener.html +++ b/target/site/apidocs/com/studentgui/app/class-use/StudentChangeListener.html @@ -1,11 +1,11 @@ - + Uses of Interface com.studentgui.app.StudentChangeListener (Vision Skills Progression Tracker 1.0.0-beta API) - + @@ -95,32 +95,32 @@

    Uses of class 
    -
    Abacus skills progression UI page.
    +
    Abacus computational skills assessment page.
    class 
    -
    Braille skills progression UI page.
    +
    Braille skills progression assessment page.
    class 
    -
    Braille note-taking skills progression page.
    +
    HumanWare BrailleNote Touch Plus (BNT+) proficiency assessment page.
    class 
    -
    Digital literacy skills progression page UI.
    +
    Digital literacy and computer skills assessment page.
    class 
    -
    Keyboarding skills page.
    +
    Touch-typing and keyboarding skills assessment page.
    class 
    -
    ScreenReader skills progression page.
    +
    Screen reader proficiency assessment page for desktop/laptop environments.
    diff --git a/target/apidocs/com/studentgui/app/package-summary.html b/target/site/apidocs/com/studentgui/app/package-summary.html similarity index 98% rename from target/apidocs/com/studentgui/app/package-summary.html rename to target/site/apidocs/com/studentgui/app/package-summary.html index d0c8f0c..41ad065 100644 --- a/target/apidocs/com/studentgui/app/package-summary.html +++ b/target/site/apidocs/com/studentgui/app/package-summary.html @@ -1,11 +1,11 @@ - + com.studentgui.app (Vision Skills Progression Tracker 1.0.0-beta API) - + diff --git a/target/apidocs/com/studentgui/app/package-tree.html b/target/site/apidocs/com/studentgui/app/package-tree.html similarity index 97% rename from target/apidocs/com/studentgui/app/package-tree.html rename to target/site/apidocs/com/studentgui/app/package-tree.html index 9fe916b..5c735ab 100644 --- a/target/apidocs/com/studentgui/app/package-tree.html +++ b/target/site/apidocs/com/studentgui/app/package-tree.html @@ -1,11 +1,11 @@ - + com.studentgui.app Class Hierarchy (Vision Skills Progression Tracker 1.0.0-beta API) - + diff --git a/target/apidocs/com/studentgui/app/package-use.html b/target/site/apidocs/com/studentgui/app/package-use.html similarity index 98% rename from target/apidocs/com/studentgui/app/package-use.html rename to target/site/apidocs/com/studentgui/app/package-use.html index aa4c865..d0cbfa9 100644 --- a/target/apidocs/com/studentgui/app/package-use.html +++ b/target/site/apidocs/com/studentgui/app/package-use.html @@ -1,11 +1,11 @@ - + Uses of Package com.studentgui.app (Vision Skills Progression Tracker 1.0.0-beta API) - + diff --git a/target/apidocs/com/studentgui/apphelpers/Database.ResultsWithDates.html b/target/site/apidocs/com/studentgui/apphelpers/Database.ResultsWithDates.html similarity index 92% rename from target/apidocs/com/studentgui/apphelpers/Database.ResultsWithDates.html rename to target/site/apidocs/com/studentgui/apphelpers/Database.ResultsWithDates.html index 37cd739..88f850a 100644 --- a/target/apidocs/com/studentgui/apphelpers/Database.ResultsWithDates.html +++ b/target/site/apidocs/com/studentgui/apphelpers/Database.ResultsWithDates.html @@ -1,11 +1,11 @@ - + Database.ResultsWithDates (Vision Skills Progression Tracker 1.0.0-beta API) - + @@ -96,7 +96,7 @@

    Class Database.Results
    Database

    -
    public static class Database.ResultsWithDates +
    public static class Database.ResultsWithDates extends Object
    Simple, immutable holder for time-series assessment results. @@ -165,14 +165,14 @@

    Field Details

  • dates

    -
    public final List<LocalDate> dates
    +
    public final List<LocalDate> dates
    Ordered session dates (oldest first). Can be empty when no sessions exist.
  • rows

    -
    public final List<List<Integer>> rows
    +
    public final List<List<Integer>> rows
    Parallel rows of integer scores. Each inner list corresponds to the assessment parts for a single session in canonical part order. May be empty when there are no sessions.
    @@ -189,7 +189,7 @@

    Constructor Details

  • ResultsWithDates

    -
    public ResultsWithDates(List<LocalDate> dates, +
    public ResultsWithDates(List<LocalDate> dates, List<List<Integer>> rows)
    Create a ResultsWithDates instance.
    diff --git a/target/apidocs/com/studentgui/apphelpers/Database.html b/target/site/apidocs/com/studentgui/apphelpers/Database.html similarity index 92% rename from target/apidocs/com/studentgui/apphelpers/Database.html rename to target/site/apidocs/com/studentgui/apphelpers/Database.html index 75a9f16..3a3f69f 100644 --- a/target/apidocs/com/studentgui/apphelpers/Database.html +++ b/target/site/apidocs/com/studentgui/apphelpers/Database.html @@ -1,11 +1,11 @@ - + Database (Vision Skills Progression Tracker 1.0.0-beta API) - + @@ -92,7 +92,7 @@

    Class Database


    -
    public class Database +
    public class Database extends Object
    Centralized database helper for the normalized SQLite schema. @@ -238,7 +238,7 @@

    Method Details

  • getOrCreateStudent

    -
    public static int getOrCreateStudent(String name) +
    public static int getOrCreateStudent(String name) throws SQLException
    Get a student id by name, creating a new Student row when none exists.
    @@ -254,7 +254,7 @@

    getOrCreateStudent

  • getOrCreateProgressType

    -
    public static int getOrCreateProgressType(String name) +
    public static int getOrCreateProgressType(String name) throws SQLException
    Get or create a ProgressType row by name.
    @@ -270,7 +270,7 @@

    getOrCreateProgressType

  • ensureAssessmentParts

    -
    public static void ensureAssessmentParts(int progressTypeId, +
    public static void ensureAssessmentParts(int progressTypeId, String[] codes) throws SQLException
    Ensure AssessmentPart rows exist for the given progress type. This uses @@ -287,7 +287,7 @@

    ensureAssessmentParts

  • cleanupAssessmentParts

    -
    public static void cleanupAssessmentParts(int progressTypeId, +
    public static void cleanupAssessmentParts(int progressTypeId, String[] allowedCodes) throws SQLException
    Remove any AssessmentPart rows for the given progress type whose code is @@ -305,7 +305,7 @@

    cleanupAssessmentParts

  • createProgressSession

    -
    public static int createProgressSession(int studentId, +
    public static int createProgressSession(int studentId, int progressTypeId, LocalDate date) throws SQLException
    @@ -325,7 +325,7 @@

    createProgressSession

  • insertAssessmentResults

    -
    public static void insertAssessmentResults(int sessionId, +
    public static void insertAssessmentResults(int sessionId, int progressTypeId, String[] codes, int[] scores) @@ -347,7 +347,7 @@

    insertAssessmentResults

  • fetchLatestAssessmentResults

    -
    public static List<List<Integer>> fetchLatestAssessmentResults(String studentName, +
    public static List<List<Integer>> fetchLatestAssessmentResults(String studentName, String progressTypeName, int limit) throws SQLException
    @@ -369,7 +369,7 @@

    fetchLatestAssessmentResults

  • fetchLatestAssessmentResultsWithDates

    -
    public static Database.ResultsWithDates fetchLatestAssessmentResultsWithDates(String studentName, +
    public static Database.ResultsWithDates fetchLatestAssessmentResultsWithDates(String studentName, String progressTypeName, int limit) throws SQLException
    @@ -390,7 +390,7 @@

    fetchLatestAssessmentResultsWithDates

  • insertKeyboardingResult

    -
    public static void insertKeyboardingResult(int sessionId, +
    public static void insertKeyboardingResult(int sessionId, String program, String topic, int speed, @@ -412,7 +412,7 @@

    insertKeyboardingResult

  • saveSessionNotes

    -
    public static void saveSessionNotes(int sessionId, +
    public static void saveSessionNotes(int sessionId, String notes) throws SQLException
    Save free-form notes for a given ProgressSession.
    @@ -428,7 +428,7 @@

    saveSessionNotes

  • saveContactLog

    -
    public static void saveContactLog(int sessionId, +
    public static void saveContactLog(int sessionId, String studentName, String date, String guardianName, @@ -463,7 +463,7 @@

    saveContactLog

  • fetchLatestContactLog

    -
    public static ContactPayload fetchLatestContactLog(String studentName) +
    public static ContactPayload fetchLatestContactLog(String studentName) throws SQLException
    Fetch the most recent ContactLog entry for the given student name. Returns a map of column names to string values, or null if none found.
    diff --git a/target/apidocs/com/studentgui/apphelpers/Helpers.html b/target/site/apidocs/com/studentgui/apphelpers/Helpers.html similarity index 89% rename from target/apidocs/com/studentgui/apphelpers/Helpers.html rename to target/site/apidocs/com/studentgui/apphelpers/Helpers.html index 55010af..9f256f7 100644 --- a/target/apidocs/com/studentgui/apphelpers/Helpers.html +++ b/target/site/apidocs/com/studentgui/apphelpers/Helpers.html @@ -1,11 +1,11 @@ - + Helpers (Vision Skills Progression Tracker 1.0.0-beta API) - + @@ -92,7 +92,7 @@

    Class Helpers


    -
    public class Helpers +
    public class Helpers extends Object
    Miscellaneous filesystem and small utility helpers used by the UI pages. @@ -221,35 +221,35 @@

    Field Details

  • PROJECT_ROOT

    -
    public static final Path PROJECT_ROOT
    +
    public static final Path PROJECT_ROOT
    The project working directory (where the process was started).
  • APP_HOME

    -
    public static final Path APP_HOME
    +
    public static final Path APP_HOME
    Application home used for storing app-specific files (defaults to ./app_home).
  • DATA_ROOT

    -
    public static final Path DATA_ROOT
    +
    public static final Path DATA_ROOT
    Root directory for persisted application data (alias of APP_HOME).
  • DATABASE_ROOT

    -
    public static final Path DATABASE_ROOT
    +
    public static final Path DATABASE_ROOT
    Directory that holds the database file.
  • DATABASE_PATH

    -
    public static final Path DATABASE_PATH
    +
    public static final Path DATABASE_PATH
    Canonical database file path used by SQLite operations.
  • @@ -264,7 +264,7 @@

    Method Details

  • setStartDir

    -
    public static void setStartDir()
    +
    public static void setStartDir()
    Attempt to set the JVM working directory to APP_HOME. Fails silently if the property cannot be set in the running environment.
    @@ -272,7 +272,7 @@

    setStartDir

  • workingDir

    -
    public static void workingDir()
    +
    public static void workingDir()
    Ensure the working data directory exists under APP_HOME. This is idempotent and safe to call on startup.
    @@ -280,7 +280,7 @@

    workingDir

  • createFolderHierarchy

    -
    public static void createFolderHierarchy()
    +
    public static void createFolderHierarchy()
    Create a basic folder hierarchy under DATA_ROOT for each student. This will create StudentDataFiles, backups and errorLogs and a per-student folder with subfolders for data sheets and materials.
    @@ -289,7 +289,7 @@

    createFolderHierarchy

  • safeName

    -
    public static String safeName(String s)
    +
    public static String safeName(String s)
    Public safe name helper for filesystem paths. Mirrors the internal sanitize implementation but is callable from other packages.
    @@ -303,7 +303,7 @@

    safeName

  • latestPlotPath

    -
    public static Path latestPlotPath(String studentName, +
    public static Path latestPlotPath(String studentName, String prefix)
    Find the latest PNG plot file for a named student with the given prefix. Returns null when no matching files exist.
    @@ -319,7 +319,7 @@

    latestPlotPath

  • studentPlotsDir

    -
    public static Path studentPlotsDir(String studentName)
    +
    public static Path studentPlotsDir(String studentName)
    Return the per-student plots directory path (APP_HOME/StudentDataFiles/{safeName}/plots).
    Parameters:
    @@ -332,7 +332,7 @@

    studentPlotsDir

  • studentReportsDir

    -
    public static Path studentReportsDir(String studentName)
    +
    public static Path studentReportsDir(String studentName)
    Return the per-student reports directory path (APP_HOME/StudentDataFiles/{safeName}/reports).
    Parameters:
    @@ -345,7 +345,7 @@

    studentReportsDir

  • studentCollectedDataDir

    -
    public static Path studentCollectedDataDir(String studentName)
    +
    public static Path studentCollectedDataDir(String studentName)
    Return the per-student collected data directory path (APP_HOME/StudentDataFiles/{safeName}/collected_data).
    Parameters:
    @@ -358,7 +358,7 @@

    studentCollectedDataDir

  • getStudents

    -
    public static List<String> getStudents()
    +
    public static List<String> getStudents()
    Attempt to return a simple list of students from PROJECT_ROOT/json_Files/students.json. Falls back to a single 'Test Student' entry when the file is missing or cannot be read.
    @@ -370,7 +370,7 @@

    getStudents

  • defaultStudent

    -
    public static String defaultStudent()
    +
    public static String defaultStudent()
    Return the default student to use when none is provided by the caller. This is the first entry from getStudents() or a sensible fallback when the roster is empty.
    diff --git a/target/apidocs/com/studentgui/apphelpers/PythonPlotter.html b/target/site/apidocs/com/studentgui/apphelpers/PythonPlotter.html similarity index 95% rename from target/apidocs/com/studentgui/apphelpers/PythonPlotter.html rename to target/site/apidocs/com/studentgui/apphelpers/PythonPlotter.html index b82a7d8..a4890de 100644 --- a/target/apidocs/com/studentgui/apphelpers/PythonPlotter.html +++ b/target/site/apidocs/com/studentgui/apphelpers/PythonPlotter.html @@ -1,11 +1,11 @@ - + PythonPlotter (Vision Skills Progression Tracker 1.0.0-beta API) - + @@ -92,7 +92,7 @@

    Class PythonPlotter


  • -
    public class PythonPlotter +
    public class PythonPlotter extends Object
    Helper to invoke the repository's Python plot runner asynchronously.

    @@ -141,7 +141,7 @@

    Method Details

  • runPlotAsync

    -
    public static void runPlotAsync(String moduleName, +
    public static void runPlotAsync(String moduleName, String studentName, Consumer<String> onComplete)
    Run the python runner for the given module and student name in a background thread. diff --git a/target/apidocs/com/studentgui/apphelpers/SessionJsonWriter.html b/target/site/apidocs/com/studentgui/apphelpers/SessionJsonWriter.html similarity index 91% rename from target/apidocs/com/studentgui/apphelpers/SessionJsonWriter.html rename to target/site/apidocs/com/studentgui/apphelpers/SessionJsonWriter.html index ffa4405..5deed83 100644 --- a/target/apidocs/com/studentgui/apphelpers/SessionJsonWriter.html +++ b/target/site/apidocs/com/studentgui/apphelpers/SessionJsonWriter.html @@ -1,11 +1,11 @@ - + SessionJsonWriter (Vision Skills Progression Tracker 1.0.0-beta API) - + @@ -92,7 +92,7 @@

    Class SessionJsonWriter


    -
    public final class SessionJsonWriter +
    public final class SessionJsonWriter extends Object
    Helper to write per-session JSON exports for app pages.
    @@ -162,7 +162,7 @@

    Method Details

  • writeSessionJson

    -
    public static Path writeSessionJson(String student, +
    public static Path writeSessionJson(String student, String pageName, Object payload)
    Write a per-session JSON file into the student's StudentDataFiles folder. @@ -180,7 +180,7 @@

    writeSessionJson

  • writeSessionJson

    -
    public static Path writeSessionJson(String student, +
    public static Path writeSessionJson(String student, String pageName, Object payload, String explicitSessionId)
    @@ -204,7 +204,7 @@

    writeSessionJson

  • writeSessionJson

    -
    public static Path writeSessionJson(String student, +
    public static Path writeSessionJson(String student, String pageName, Object payload, int explicitSessionId)
    @@ -224,7 +224,7 @@

    writeSessionJson

  • writeSessionJson

    -
    public static Path writeSessionJson(String student, +
    public static Path writeSessionJson(String student, String pageName, String[] codes, int[] scores)
    diff --git a/target/apidocs/com/studentgui/apphelpers/Settings.html b/target/site/apidocs/com/studentgui/apphelpers/Settings.html similarity index 92% rename from target/apidocs/com/studentgui/apphelpers/Settings.html rename to target/site/apidocs/com/studentgui/apphelpers/Settings.html index a368c27..6083b20 100644 --- a/target/apidocs/com/studentgui/apphelpers/Settings.html +++ b/target/site/apidocs/com/studentgui/apphelpers/Settings.html @@ -1,11 +1,11 @@ - + Settings (Vision Skills Progression Tracker 1.0.0-beta API) - + @@ -92,7 +92,7 @@

    Class Settings


    -
    public final class Settings +
    public final class Settings extends Object
    Lightweight settings persistence for simple key/value preferences.
    @@ -141,7 +141,7 @@

    Method Details

  • get

    -
    public static String get(String key, +
    public static String get(String key, String def)
    Get a persisted setting value or return a default when missing.
    @@ -156,7 +156,7 @@

    get

  • put

    -
    public static void put(String key, +
    public static void put(String key, String value)
    Store a setting value and persist to disk immediately.
    diff --git a/target/apidocs/com/studentgui/apphelpers/SqlGenerate.html b/target/site/apidocs/com/studentgui/apphelpers/SqlGenerate.html similarity index 96% rename from target/apidocs/com/studentgui/apphelpers/SqlGenerate.html rename to target/site/apidocs/com/studentgui/apphelpers/SqlGenerate.html index 737a20c..d16f65b 100644 --- a/target/apidocs/com/studentgui/apphelpers/SqlGenerate.html +++ b/target/site/apidocs/com/studentgui/apphelpers/SqlGenerate.html @@ -1,11 +1,11 @@ - + SqlGenerate (Vision Skills Progression Tracker 1.0.0-beta API) - + @@ -92,7 +92,7 @@

    Class SqlGenerate


    -
    public class SqlGenerate +
    public class SqlGenerate extends Object
    Utility responsible for creating/validating the on-disk SQLite database and canonical schema used by the application. Safe to call multiple times.
    @@ -135,7 +135,7 @@

    Method Details

  • initializeDatabase

    -
    public static void initializeDatabase()
    +
    public static void initializeDatabase()
    Ensure the database file and canonical schema exist. This method is idempotent and safe to call on application startup. It will create the parent folder for the DB file if necessary and apply the embedded SCHEMA statements.
    diff --git a/target/apidocs/com/studentgui/apphelpers/UiNotifier.html b/target/site/apidocs/com/studentgui/apphelpers/UiNotifier.html similarity index 94% rename from target/apidocs/com/studentgui/apphelpers/UiNotifier.html rename to target/site/apidocs/com/studentgui/apphelpers/UiNotifier.html index 5c20a7a..191b8f6 100644 --- a/target/apidocs/com/studentgui/apphelpers/UiNotifier.html +++ b/target/site/apidocs/com/studentgui/apphelpers/UiNotifier.html @@ -1,11 +1,11 @@ - + UiNotifier (Vision Skills Progression Tracker 1.0.0-beta API) - + @@ -92,7 +92,7 @@

    Class UiNotifier


  • -
    public class UiNotifier +
    public class UiNotifier extends Object
    Very small non-modal notification window for quick status messages. @@ -137,7 +137,7 @@

    Method Details

  • show

    -
    public static void show(String message)
    +
    public static void show(String message)
    Display a short, transient notification message on screen.
    Parameters:
    diff --git a/target/apidocs/com/studentgui/apphelpers/class-use/Database.ResultsWithDates.html b/target/site/apidocs/com/studentgui/apphelpers/class-use/Database.ResultsWithDates.html similarity index 97% rename from target/apidocs/com/studentgui/apphelpers/class-use/Database.ResultsWithDates.html rename to target/site/apidocs/com/studentgui/apphelpers/class-use/Database.ResultsWithDates.html index b2b5d92..800d940 100644 --- a/target/apidocs/com/studentgui/apphelpers/class-use/Database.ResultsWithDates.html +++ b/target/site/apidocs/com/studentgui/apphelpers/class-use/Database.ResultsWithDates.html @@ -1,11 +1,11 @@ - + Uses of Class com.studentgui.apphelpers.Database.ResultsWithDates (Vision Skills Progression Tracker 1.0.0-beta API) - + diff --git a/target/apidocs/com/studentgui/apphelpers/class-use/Database.html b/target/site/apidocs/com/studentgui/apphelpers/class-use/Database.html similarity index 96% rename from target/apidocs/com/studentgui/apphelpers/class-use/Database.html rename to target/site/apidocs/com/studentgui/apphelpers/class-use/Database.html index 78cb30d..d2df999 100644 --- a/target/apidocs/com/studentgui/apphelpers/class-use/Database.html +++ b/target/site/apidocs/com/studentgui/apphelpers/class-use/Database.html @@ -1,11 +1,11 @@ - + Uses of Class com.studentgui.apphelpers.Database (Vision Skills Progression Tracker 1.0.0-beta API) - + diff --git a/target/apidocs/com/studentgui/apphelpers/class-use/Helpers.html b/target/site/apidocs/com/studentgui/apphelpers/class-use/Helpers.html similarity index 96% rename from target/apidocs/com/studentgui/apphelpers/class-use/Helpers.html rename to target/site/apidocs/com/studentgui/apphelpers/class-use/Helpers.html index 2f7ed7f..6873edd 100644 --- a/target/apidocs/com/studentgui/apphelpers/class-use/Helpers.html +++ b/target/site/apidocs/com/studentgui/apphelpers/class-use/Helpers.html @@ -1,11 +1,11 @@ - + Uses of Class com.studentgui.apphelpers.Helpers (Vision Skills Progression Tracker 1.0.0-beta API) - + diff --git a/target/apidocs/com/studentgui/apphelpers/class-use/PythonPlotter.html b/target/site/apidocs/com/studentgui/apphelpers/class-use/PythonPlotter.html similarity index 96% rename from target/apidocs/com/studentgui/apphelpers/class-use/PythonPlotter.html rename to target/site/apidocs/com/studentgui/apphelpers/class-use/PythonPlotter.html index 7162ca5..6a2a370 100644 --- a/target/apidocs/com/studentgui/apphelpers/class-use/PythonPlotter.html +++ b/target/site/apidocs/com/studentgui/apphelpers/class-use/PythonPlotter.html @@ -1,11 +1,11 @@ - + Uses of Class com.studentgui.apphelpers.PythonPlotter (Vision Skills Progression Tracker 1.0.0-beta API) - + diff --git a/target/apidocs/com/studentgui/apphelpers/class-use/SessionJsonWriter.html b/target/site/apidocs/com/studentgui/apphelpers/class-use/SessionJsonWriter.html similarity index 96% rename from target/apidocs/com/studentgui/apphelpers/class-use/SessionJsonWriter.html rename to target/site/apidocs/com/studentgui/apphelpers/class-use/SessionJsonWriter.html index 8c5aa51..833583c 100644 --- a/target/apidocs/com/studentgui/apphelpers/class-use/SessionJsonWriter.html +++ b/target/site/apidocs/com/studentgui/apphelpers/class-use/SessionJsonWriter.html @@ -1,11 +1,11 @@ - + Uses of Class com.studentgui.apphelpers.SessionJsonWriter (Vision Skills Progression Tracker 1.0.0-beta API) - + diff --git a/target/apidocs/com/studentgui/apphelpers/class-use/Settings.html b/target/site/apidocs/com/studentgui/apphelpers/class-use/Settings.html similarity index 96% rename from target/apidocs/com/studentgui/apphelpers/class-use/Settings.html rename to target/site/apidocs/com/studentgui/apphelpers/class-use/Settings.html index 9ee3ad6..76548ce 100644 --- a/target/apidocs/com/studentgui/apphelpers/class-use/Settings.html +++ b/target/site/apidocs/com/studentgui/apphelpers/class-use/Settings.html @@ -1,11 +1,11 @@ - + Uses of Class com.studentgui.apphelpers.Settings (Vision Skills Progression Tracker 1.0.0-beta API) - + diff --git a/target/apidocs/com/studentgui/apphelpers/class-use/SqlGenerate.html b/target/site/apidocs/com/studentgui/apphelpers/class-use/SqlGenerate.html similarity index 96% rename from target/apidocs/com/studentgui/apphelpers/class-use/SqlGenerate.html rename to target/site/apidocs/com/studentgui/apphelpers/class-use/SqlGenerate.html index 6d3f067..3531331 100644 --- a/target/apidocs/com/studentgui/apphelpers/class-use/SqlGenerate.html +++ b/target/site/apidocs/com/studentgui/apphelpers/class-use/SqlGenerate.html @@ -1,11 +1,11 @@ - + Uses of Class com.studentgui.apphelpers.SqlGenerate (Vision Skills Progression Tracker 1.0.0-beta API) - + diff --git a/target/apidocs/com/studentgui/apphelpers/class-use/UiNotifier.html b/target/site/apidocs/com/studentgui/apphelpers/class-use/UiNotifier.html similarity index 96% rename from target/apidocs/com/studentgui/apphelpers/class-use/UiNotifier.html rename to target/site/apidocs/com/studentgui/apphelpers/class-use/UiNotifier.html index 199b43c..af93dd3 100644 --- a/target/apidocs/com/studentgui/apphelpers/class-use/UiNotifier.html +++ b/target/site/apidocs/com/studentgui/apphelpers/class-use/UiNotifier.html @@ -1,11 +1,11 @@ - + Uses of Class com.studentgui.apphelpers.UiNotifier (Vision Skills Progression Tracker 1.0.0-beta API) - + diff --git a/target/apidocs/com/studentgui/apphelpers/dto/AssessmentPayload.html b/target/site/apidocs/com/studentgui/apphelpers/dto/AssessmentPayload.html similarity index 92% rename from target/apidocs/com/studentgui/apphelpers/dto/AssessmentPayload.html rename to target/site/apidocs/com/studentgui/apphelpers/dto/AssessmentPayload.html index 4b1fae4..3d15f40 100644 --- a/target/apidocs/com/studentgui/apphelpers/dto/AssessmentPayload.html +++ b/target/site/apidocs/com/studentgui/apphelpers/dto/AssessmentPayload.html @@ -1,11 +1,11 @@ - + AssessmentPayload (Vision Skills Progression Tracker 1.0.0-beta API) - + @@ -96,7 +96,7 @@

    Class AssessmentPayload

    SessionPayload

    -
    public class AssessmentPayload +
    public class AssessmentPayload extends Object implements SessionPayload
    Typed payload for assessment-style pages (codes + scores).
    @@ -190,21 +190,21 @@

    Field Details

  • sessionId

    -
    public int sessionId
    +
    public int sessionId
    Database session id for this payload.
  • codes

    -
    public String[] codes
    +
    public String[] codes
    Array of part codes (e.g. "P1_1").
  • scores

    -
    public int[] scores
    +
    public int[] scores
    Parallel array of integer scores.
  • @@ -219,14 +219,14 @@

    Constructor Details

  • AssessmentPayload

    -
    public AssessmentPayload()
    +
    No-arg constructor for Jackson and tests.
  • AssessmentPayload

    -
    public AssessmentPayload(int sessionIdParam, +
    public AssessmentPayload(int sessionIdParam, String[] codesParam, int[] scoresParam)
    Create an assessment payload.
    @@ -249,7 +249,7 @@

    Method Details

  • getSessionId

    -
    public int getSessionId()
    +
    public int getSessionId()
    Description copied from interface: SessionPayload
    Return the database session id associated with this payload.
    @@ -263,7 +263,7 @@

    getSessionId

  • toString

    -
    public String toString()
    +
    public String toString()
    Overrides:
    toString in class Object
    diff --git a/target/apidocs/com/studentgui/apphelpers/dto/ContactPayload.html b/target/site/apidocs/com/studentgui/apphelpers/dto/ContactPayload.html similarity index 91% rename from target/apidocs/com/studentgui/apphelpers/dto/ContactPayload.html rename to target/site/apidocs/com/studentgui/apphelpers/dto/ContactPayload.html index fef5322..aa955a7 100644 --- a/target/apidocs/com/studentgui/apphelpers/dto/ContactPayload.html +++ b/target/site/apidocs/com/studentgui/apphelpers/dto/ContactPayload.html @@ -1,11 +1,11 @@ - + ContactPayload (Vision Skills Progression Tracker 1.0.0-beta API) - + @@ -96,7 +96,7 @@

    Class ContactPayload

    SessionPayload

    -
    public class ContactPayload +
    public class ContactPayload extends Object implements SessionPayload
    Typed payload for contact log entries.
    @@ -223,63 +223,63 @@

    Field Details

  • sessionId

    -
    public int sessionId
    +
    public int sessionId
    Database session id.
  • guardian

    -
    public String guardian
    +
    public String guardian
    Guardian/parent name.
  • method

    -
    public String method
    +
    public String method
    Method of contact (Phone/Email/etc).
  • phone

    -
    public String phone
    +
    public String phone
    Phone number.
  • email

    -
    public String email
    +
    public String email
    Email address.
  • response

    -
    public String response
    +
    public String response
    Brief response summary.
  • general

    -
    public String general
    +
    public String general
    High-level general notes.
  • specific

    -
    public String specific
    +
    public String specific
    Specific action items or points.
  • notes

    -
    public String notes
    +
    public String notes
    Full notes text.
  • @@ -294,14 +294,14 @@

    Constructor Details

  • ContactPayload

    -
    public ContactPayload()
    +
    public ContactPayload()
    No-arg constructor for Jackson.
  • ContactPayload

    -
    public ContactPayload(int sessionIdParam, +
    public ContactPayload(int sessionIdParam, String guardianParam, String methodParam, String phoneParam, @@ -336,7 +336,7 @@

    Method Details

  • getSessionId

    -
    public int getSessionId()
    +
    public int getSessionId()
    Description copied from interface: SessionPayload
    Return the database session id associated with this payload.
    diff --git a/target/apidocs/com/studentgui/apphelpers/dto/KeyboardingPayload.html b/target/site/apidocs/com/studentgui/apphelpers/dto/KeyboardingPayload.html similarity index 91% rename from target/apidocs/com/studentgui/apphelpers/dto/KeyboardingPayload.html rename to target/site/apidocs/com/studentgui/apphelpers/dto/KeyboardingPayload.html index 78f183a..153b2bc 100644 --- a/target/apidocs/com/studentgui/apphelpers/dto/KeyboardingPayload.html +++ b/target/site/apidocs/com/studentgui/apphelpers/dto/KeyboardingPayload.html @@ -1,11 +1,11 @@ - + KeyboardingPayload (Vision Skills Progression Tracker 1.0.0-beta API) - + @@ -96,7 +96,7 @@

    Class KeyboardingPayload

    SessionPayload

    -
    public class KeyboardingPayload +
    public class KeyboardingPayload extends Object implements SessionPayload
    Typed payload for Keyboarding page.
    @@ -199,35 +199,35 @@

    Field Details

  • sessionId

    -
    public int sessionId
    +
    public int sessionId
    Database session id.
  • program

    -
    public String program
    +
    public String program
    Program or curriculum name.
  • topic

    -
    public String topic
    +
    public String topic
    Topic or lesson name.
  • speed

    -
    public int speed
    +
    public int speed
    Speed in WPM.
  • accuracy

    -
    public int accuracy
    +
    public int accuracy
    Accuracy percentage.
  • @@ -242,14 +242,14 @@

    Constructor Details

  • KeyboardingPayload

    -
    public KeyboardingPayload()
    +
    No-arg constructor for Jackson.
  • KeyboardingPayload

    -
    public KeyboardingPayload(int sessionIdParam, +
    public KeyboardingPayload(int sessionIdParam, String programParam, String topicParam, int speedParam, @@ -276,7 +276,7 @@

    Method Details

  • getSessionId

    -
    public int getSessionId()
    +
    public int getSessionId()
    Description copied from interface: SessionPayload
    Return the database session id associated with this payload.
    diff --git a/target/apidocs/com/studentgui/apphelpers/dto/NotesPayload.html b/target/site/apidocs/com/studentgui/apphelpers/dto/NotesPayload.html similarity index 93% rename from target/apidocs/com/studentgui/apphelpers/dto/NotesPayload.html rename to target/site/apidocs/com/studentgui/apphelpers/dto/NotesPayload.html index d9b21c8..bb834af 100644 --- a/target/apidocs/com/studentgui/apphelpers/dto/NotesPayload.html +++ b/target/site/apidocs/com/studentgui/apphelpers/dto/NotesPayload.html @@ -1,11 +1,11 @@ - + NotesPayload (Vision Skills Progression Tracker 1.0.0-beta API) - + @@ -96,7 +96,7 @@

    Class NotesPayload

    SessionPayload

    -
    public class NotesPayload +
    public class NotesPayload extends Object implements SessionPayload
    Typed payload for freeform notes pages.
    @@ -181,14 +181,14 @@

    Field Details

  • sessionId

    -
    public int sessionId
    +
    public int sessionId
    Database session id.
  • notes

    -
    public String notes
    +
    public String notes
    The freeform notes text.
  • @@ -203,14 +203,14 @@

    Constructor Details

  • NotesPayload

    -
    public NotesPayload()
    +
    public NotesPayload()
    No-arg constructor for Jackson.
  • NotesPayload

    -
    public NotesPayload(int sessionIdParam, +
    public NotesPayload(int sessionIdParam, String notesParam)
    Create a notes payload.
    @@ -231,7 +231,7 @@

    Method Details

  • getSessionId

    -
    public int getSessionId()
    +
    public int getSessionId()
    Description copied from interface: SessionPayload
    Return the database session id associated with this payload.
    diff --git a/target/apidocs/com/studentgui/apphelpers/dto/SessionPayload.html b/target/site/apidocs/com/studentgui/apphelpers/dto/SessionPayload.html similarity index 94% rename from target/apidocs/com/studentgui/apphelpers/dto/SessionPayload.html rename to target/site/apidocs/com/studentgui/apphelpers/dto/SessionPayload.html index 0b98924..2ba562f 100644 --- a/target/apidocs/com/studentgui/apphelpers/dto/SessionPayload.html +++ b/target/site/apidocs/com/studentgui/apphelpers/dto/SessionPayload.html @@ -1,11 +1,11 @@ - + SessionPayload (Vision Skills Progression Tracker 1.0.0-beta API) - + @@ -93,7 +93,7 @@

    Interface SessionPayload

    AssessmentPayload, ContactPayload, KeyboardingPayload, NotesPayload

    -
    public interface SessionPayload
    +
    public interface SessionPayload
    Common interface for session-scoped payloads that carry a DB session id.
    @@ -131,7 +131,7 @@

    Method Details

  • getSessionId

    -
    int getSessionId()
    +
    Return the database session id associated with this payload.
    Returns:
    diff --git a/target/apidocs/com/studentgui/apphelpers/dto/class-use/AssessmentPayload.html b/target/site/apidocs/com/studentgui/apphelpers/dto/class-use/AssessmentPayload.html similarity index 96% rename from target/apidocs/com/studentgui/apphelpers/dto/class-use/AssessmentPayload.html rename to target/site/apidocs/com/studentgui/apphelpers/dto/class-use/AssessmentPayload.html index 136d50e..bb2a779 100644 --- a/target/apidocs/com/studentgui/apphelpers/dto/class-use/AssessmentPayload.html +++ b/target/site/apidocs/com/studentgui/apphelpers/dto/class-use/AssessmentPayload.html @@ -1,11 +1,11 @@ - + Uses of Class com.studentgui.apphelpers.dto.AssessmentPayload (Vision Skills Progression Tracker 1.0.0-beta API) - + diff --git a/target/apidocs/com/studentgui/apphelpers/dto/class-use/ContactPayload.html b/target/site/apidocs/com/studentgui/apphelpers/dto/class-use/ContactPayload.html similarity index 97% rename from target/apidocs/com/studentgui/apphelpers/dto/class-use/ContactPayload.html rename to target/site/apidocs/com/studentgui/apphelpers/dto/class-use/ContactPayload.html index 9fe3f97..bcc63c5 100644 --- a/target/apidocs/com/studentgui/apphelpers/dto/class-use/ContactPayload.html +++ b/target/site/apidocs/com/studentgui/apphelpers/dto/class-use/ContactPayload.html @@ -1,11 +1,11 @@ - + Uses of Class com.studentgui.apphelpers.dto.ContactPayload (Vision Skills Progression Tracker 1.0.0-beta API) - + diff --git a/target/apidocs/com/studentgui/apphelpers/dto/class-use/KeyboardingPayload.html b/target/site/apidocs/com/studentgui/apphelpers/dto/class-use/KeyboardingPayload.html similarity index 96% rename from target/apidocs/com/studentgui/apphelpers/dto/class-use/KeyboardingPayload.html rename to target/site/apidocs/com/studentgui/apphelpers/dto/class-use/KeyboardingPayload.html index ad3799a..9ac047d 100644 --- a/target/apidocs/com/studentgui/apphelpers/dto/class-use/KeyboardingPayload.html +++ b/target/site/apidocs/com/studentgui/apphelpers/dto/class-use/KeyboardingPayload.html @@ -1,11 +1,11 @@ - + Uses of Class com.studentgui.apphelpers.dto.KeyboardingPayload (Vision Skills Progression Tracker 1.0.0-beta API) - + diff --git a/target/apidocs/com/studentgui/apphelpers/dto/class-use/NotesPayload.html b/target/site/apidocs/com/studentgui/apphelpers/dto/class-use/NotesPayload.html similarity index 96% rename from target/apidocs/com/studentgui/apphelpers/dto/class-use/NotesPayload.html rename to target/site/apidocs/com/studentgui/apphelpers/dto/class-use/NotesPayload.html index 583dd8d..f83a7bb 100644 --- a/target/apidocs/com/studentgui/apphelpers/dto/class-use/NotesPayload.html +++ b/target/site/apidocs/com/studentgui/apphelpers/dto/class-use/NotesPayload.html @@ -1,11 +1,11 @@ - + Uses of Class com.studentgui.apphelpers.dto.NotesPayload (Vision Skills Progression Tracker 1.0.0-beta API) - + diff --git a/target/apidocs/com/studentgui/apphelpers/dto/class-use/SessionPayload.html b/target/site/apidocs/com/studentgui/apphelpers/dto/class-use/SessionPayload.html similarity index 98% rename from target/apidocs/com/studentgui/apphelpers/dto/class-use/SessionPayload.html rename to target/site/apidocs/com/studentgui/apphelpers/dto/class-use/SessionPayload.html index 41a383f..a83b266 100644 --- a/target/apidocs/com/studentgui/apphelpers/dto/class-use/SessionPayload.html +++ b/target/site/apidocs/com/studentgui/apphelpers/dto/class-use/SessionPayload.html @@ -1,11 +1,11 @@ - + Uses of Interface com.studentgui.apphelpers.dto.SessionPayload (Vision Skills Progression Tracker 1.0.0-beta API) - + diff --git a/target/apidocs/com/studentgui/apphelpers/dto/package-summary.html b/target/site/apidocs/com/studentgui/apphelpers/dto/package-summary.html similarity index 98% rename from target/apidocs/com/studentgui/apphelpers/dto/package-summary.html rename to target/site/apidocs/com/studentgui/apphelpers/dto/package-summary.html index b9edca6..eb88afe 100644 --- a/target/apidocs/com/studentgui/apphelpers/dto/package-summary.html +++ b/target/site/apidocs/com/studentgui/apphelpers/dto/package-summary.html @@ -1,11 +1,11 @@ - + com.studentgui.apphelpers.dto (Vision Skills Progression Tracker 1.0.0-beta API) - + diff --git a/target/apidocs/com/studentgui/apphelpers/dto/package-tree.html b/target/site/apidocs/com/studentgui/apphelpers/dto/package-tree.html similarity index 97% rename from target/apidocs/com/studentgui/apphelpers/dto/package-tree.html rename to target/site/apidocs/com/studentgui/apphelpers/dto/package-tree.html index ac45bca..f8c2835 100644 --- a/target/apidocs/com/studentgui/apphelpers/dto/package-tree.html +++ b/target/site/apidocs/com/studentgui/apphelpers/dto/package-tree.html @@ -1,11 +1,11 @@ - + com.studentgui.apphelpers.dto Class Hierarchy (Vision Skills Progression Tracker 1.0.0-beta API) - + diff --git a/target/apidocs/com/studentgui/apphelpers/dto/package-use.html b/target/site/apidocs/com/studentgui/apphelpers/dto/package-use.html similarity index 97% rename from target/apidocs/com/studentgui/apphelpers/dto/package-use.html rename to target/site/apidocs/com/studentgui/apphelpers/dto/package-use.html index c0c0c7b..9dd75b5 100644 --- a/target/apidocs/com/studentgui/apphelpers/dto/package-use.html +++ b/target/site/apidocs/com/studentgui/apphelpers/dto/package-use.html @@ -1,11 +1,11 @@ - + Uses of Package com.studentgui.apphelpers.dto (Vision Skills Progression Tracker 1.0.0-beta API) - + diff --git a/target/apidocs/com/studentgui/apphelpers/package-summary.html b/target/site/apidocs/com/studentgui/apphelpers/package-summary.html similarity index 98% rename from target/apidocs/com/studentgui/apphelpers/package-summary.html rename to target/site/apidocs/com/studentgui/apphelpers/package-summary.html index 9e26866..01a9e72 100644 --- a/target/apidocs/com/studentgui/apphelpers/package-summary.html +++ b/target/site/apidocs/com/studentgui/apphelpers/package-summary.html @@ -1,11 +1,11 @@ - + com.studentgui.apphelpers (Vision Skills Progression Tracker 1.0.0-beta API) - + diff --git a/target/apidocs/com/studentgui/apphelpers/package-tree.html b/target/site/apidocs/com/studentgui/apphelpers/package-tree.html similarity index 97% rename from target/apidocs/com/studentgui/apphelpers/package-tree.html rename to target/site/apidocs/com/studentgui/apphelpers/package-tree.html index 3e33eb9..a706bd8 100644 --- a/target/apidocs/com/studentgui/apphelpers/package-tree.html +++ b/target/site/apidocs/com/studentgui/apphelpers/package-tree.html @@ -1,11 +1,11 @@ - + com.studentgui.apphelpers Class Hierarchy (Vision Skills Progression Tracker 1.0.0-beta API) - + diff --git a/target/apidocs/com/studentgui/apphelpers/package-use.html b/target/site/apidocs/com/studentgui/apphelpers/package-use.html similarity index 97% rename from target/apidocs/com/studentgui/apphelpers/package-use.html rename to target/site/apidocs/com/studentgui/apphelpers/package-use.html index 2746f98..e10cff6 100644 --- a/target/apidocs/com/studentgui/apphelpers/package-use.html +++ b/target/site/apidocs/com/studentgui/apphelpers/package-use.html @@ -1,11 +1,11 @@ - + Uses of Package com.studentgui.apphelpers (Vision Skills Progression Tracker 1.0.0-beta API) - + diff --git a/target/apidocs/com/studentgui/apppages/Abacus.html b/target/site/apidocs/com/studentgui/apppages/Abacus.html similarity index 95% rename from target/apidocs/com/studentgui/apppages/Abacus.html rename to target/site/apidocs/com/studentgui/apppages/Abacus.html index 742f985..a6b9765 100644 --- a/target/apidocs/com/studentgui/apppages/Abacus.html +++ b/target/site/apidocs/com/studentgui/apppages/Abacus.html @@ -1,11 +1,11 @@ - + Abacus (Vision Skills Progression Tracker 1.0.0-beta API) - + @@ -104,20 +104,52 @@

    Class Abacus

    DateChangeListener, StudentChangeListener, ImageObserver, MenuContainer, Serializable, Accessible

    -
    public class Abacus +
    public class Abacus extends JPanel implements DateChangeListener, StudentChangeListener
    -
    Abacus skills progression UI page. -

    - Presents a scrollable list of abacus-related skill input fields for a - particular student and date. Values entered here are persisted via the - centralized database helper into the normalized schema and can be plotted - using the shared JLineGraph component. -

    +
    Abacus computational skills assessment page. + +

    Provides a structured interface for evaluating student proficiency with the Cranmer + Abacus across 22 standardized skills organized into 8 progressive competency phases:

    + +
      +
    • Phase 1 (P1_1–P1_4): Foundational bead manipulation (setting, clearing, place value, vocabulary)
    • +
    • Phase 2 (P2_1–P2_3): Single-digit addition (direct and indirect methods)
    • +
    • Phase 3 (P3_1–P3_3): Single-digit subtraction (direct and indirect methods)
    • +
    • Phase 4 (P4_1–P4_2): Multiplication with multi-digit operands
    • +
    • Phase 5 (P5_1–P5_2): Division with multi-digit operands
    • +
    • Phase 6 (P6_1–P6_4): Decimal arithmetic (all four operations)
    • +
    • Phase 7 (P7_1–P7_4): Fraction arithmetic (all four operations)
    • +
    • Phase 8 (P8_1–P8_2): Advanced operations (percentages, square roots)
    • +
    + +

    Data Persistence and Export:

    +
      +
    • Skill scores are captured via PhaseScoreField components (integer 0–4 typical)
    • +
    • Submit button persists values to normalized schema using Database.insertAssessmentResults(int, int, java.lang.String[], int[])
    • +
    • Session data exported to timestamped JSON in StudentDataFiles/<student>/Sessions/Abacus/
    • +
    • Per-phase time-series plots generated and saved to plots/ directory
    • +
    • Comprehensive Markdown and HTML reports generated with embedded phase plots and color-coded legends
    • +
    + +

    Report Artifacts:

    +
      +
    • JSON export: Abacus-<sessionId>-<timestamp>.json with session envelope
    • +
    • Phase group plots: Abacus-<sessionId>-<date>-P<N>.png (8 PNG images)
    • +
    • Markdown report: reports/Abacus-<sessionId>-<date>.md with relative image links
    • +
    • HTML report: reports/Abacus-<sessionId>-<date>.html with inline styles and legends
    • +
    + +

    The shared JLineGraph visualizes recent session trends, grouping skills by phase prefix + to maintain chart readability. Implements DateChangeListener and + StudentChangeListener for dynamic updates when global selections change.

    See Also:
    @@ -228,7 +260,7 @@

    Constructor Details

  • Abacus

    -
    public Abacus(String studentName, +
    public Abacus(String studentName, LocalDate date, JLineGraph lineGraph)
    Construct the Abacus page for the given student and session date.
    @@ -251,7 +283,7 @@

    Method Details

  • dateChanged

    -
    public void dateChanged(LocalDate newDate)
    +
    public void dateChanged(LocalDate newDate)
    Description copied from interface: DateChangeListener
    Called when the application date has been changed by the user.
    @@ -265,7 +297,7 @@

    dateChanged

  • studentChanged

    -
    public void studentChanged(String newStudent)
    +
    public void studentChanged(String newStudent)
    Description copied from interface: StudentChangeListener
    Called when the application selected student has changed.
    diff --git a/target/apidocs/com/studentgui/apppages/Braille.html b/target/site/apidocs/com/studentgui/apppages/Braille.html similarity index 95% rename from target/apidocs/com/studentgui/apppages/Braille.html rename to target/site/apidocs/com/studentgui/apppages/Braille.html index a1e974a..3a927b2 100644 --- a/target/apidocs/com/studentgui/apppages/Braille.html +++ b/target/site/apidocs/com/studentgui/apppages/Braille.html @@ -1,11 +1,11 @@ - + Braille (Vision Skills Progression Tracker 1.0.0-beta API) - + @@ -104,19 +104,53 @@

    Class Braille

    DateChangeListener, StudentChangeListener, ImageObserver, MenuContainer, Serializable, Accessible

    -
    public class Braille +
    public class Braille extends JPanel implements DateChangeListener, StudentChangeListener
    -
    Braille skills progression UI page. +
    Braille skills progression assessment page. - Displays a list of braille-related skill input fields and provides controls - to persist entries to the normalized database schema. The page updates a - shared JLineGraph to visualize recent results for the selected - student.
    +

    Provides a comprehensive user interface for tracking student proficiency across + 64 standardized Braille skills organized into 8 progressive phases following the + Mangold Developmental Program sequence:

    + +
      +
    • Phase 1 (P1_1–P1_4): Foundational tracking and discrimination skills
    • +
    • Phase 2 (P2_1–P2_15): Mangold letter progression (G C L → V J)
    • +
    • Phase 3 (P3_1–P3_15): Contractions, wordsigns, and Grade 2 Braille basics
    • +
    • Phase 4 (P4_1–P4_4): Indicators (Grade 1, capitals, numeric mode, typeform)
    • +
    • Phase 5 (P5_1–P5_4): Document formatting (page numbers, headings, lists, poetry)
    • +
    • Phase 6 (P6_1–P6_7): Basic Nemeth Math Code (operations, shapes, fractions)
    • +
    • Phase 7 (P7_1–P7_8): Advanced Math (algebra, indices, radicals, functions, Greek)
    • +
    • Phase 8 (P8_1–P8_7): Higher mathematics (modifiers, calculus, probability)
    • +
    + +

    Data Flow and Persistence:

    +
      +
    • Each skill is represented by a PhaseScoreField accepting integer scores (0–4 typical range)
    • +
    • On submission, values are persisted to the normalized schema via Database.insertAssessmentResults(int, int, java.lang.String[], int[])
    • +
    • A timestamped JSON export is written to StudentDataFiles/<student>/Sessions/Braille/
    • +
    • Time-series plots are generated per phase group and saved as PNG images to plots/
    • +
    • Markdown and HTML reports are generated combining all phase plots with legend and metadata
    • +
    + +

    Generated Artifacts:

    +
      +
    • JSON session file: Braille-<sessionId>-<timestamp>.json
    • +
    • Phase plots: Braille-<sessionId>-<date>-P<N>.png (8 phase groups)
    • +
    • Markdown report: reports/Braille-<sessionId>-<date>.md
    • +
    • HTML report: reports/Braille-<sessionId>-<date>.html with embedded plots and color-coded legends
    • +
    + +

    The shared JLineGraph component visualizes recent session trends for the selected + student, grouped by phase to prevent overcrowding. This page implements DateChangeListener + and StudentChangeListener to refresh data when the global student or date selection changes.

    See Also:
    @@ -227,7 +261,7 @@

    Constructor Details

  • Braille

    -
    public Braille(String studentName, +
    public Braille(String studentName, LocalDate date, JLineGraph lineGraph)
    Construct the Braille skills page for a given student and date.
    @@ -250,7 +284,7 @@

    Method Details

  • dateChanged

    -
    public void dateChanged(LocalDate newDate)
    +
    public void dateChanged(LocalDate newDate)
    Description copied from interface: DateChangeListener
    Called when the application date has been changed by the user.
    @@ -264,7 +298,7 @@

    dateChanged

  • studentChanged

    -
    public void studentChanged(String newStudent)
    +
    public void studentChanged(String newStudent)
    Description copied from interface: StudentChangeListener
    Called when the application selected student has changed.
    diff --git a/target/apidocs/com/studentgui/apppages/BrailleNote.html b/target/site/apidocs/com/studentgui/apppages/BrailleNote.html similarity index 94% rename from target/apidocs/com/studentgui/apppages/BrailleNote.html rename to target/site/apidocs/com/studentgui/apppages/BrailleNote.html index 376d1c1..0142a74 100644 --- a/target/apidocs/com/studentgui/apppages/BrailleNote.html +++ b/target/site/apidocs/com/studentgui/apppages/BrailleNote.html @@ -1,11 +1,11 @@ - + BrailleNote (Vision Skills Progression Tracker 1.0.0-beta API) - + @@ -104,20 +104,79 @@

    Class BrailleNote

    DateChangeListener, StudentChangeListener, ImageObserver, MenuContainer, Serializable, Accessible

    -
    public class BrailleNote + -
    Braille note-taking skills progression page. -

    - Presents a scrollable list of skill fields for a student and allows - submission of scores into the canonical (normalized) SQLite schema. - The page also displays a shared JLineGraph instance to visualize - recent results. -

    +
    HumanWare BrailleNote Touch Plus (BNT+) proficiency assessment page. + +

    Evaluates student competency with the BrailleNote Touch Plus refreshable braille notetaker + and productivity device across 52 skills organized into 12 functional domains:

    + +
      +
    • Phase 1 (P1_1–P1_9): Device Fundamentals and Core Applications +
        +
      • Physical layout (braille keyboard, navigation keys, touchscreen, ports)
      • +
      • Setup procedures and universal commands (power, mode switching, context menus)
      • +
      • BNT+ navigation paradigm (gestures, quick keys, braille commands)
      • +
      • File management (folders, copy/paste, rename, delete)
      • +
      • Word processor (KeyWord): document creation, editing, formatting
      • +
      • Email (KeyMail): compose, send, receive, attachments
      • +
      • Internet browsing (KeyWeb): navigation, bookmarks, forms
      • +
      • Calculator and KeyMath (arithmetic, scientific functions)
      • +
      +
    • +
    • Phase 2 (P2_1–P2_7): Productivity Suite Applications +
        +
      • Calendar management (appointments, reminders, recurring events)
      • +
      • KeyBRF (Braille file viewer/editor)
      • +
      • KeyFiles (file explorer and organizer)
      • +
      • KeyMail (advanced email features)
      • +
      • KeyWeb (advanced browsing, accessibility modes)
      • +
      • KeyCalc (spreadsheet concepts)
      • +
      • KeyWord (advanced formatting, styles, tables)
      • +
      +
    • +
    • Phase 3 (P3_1–P3_7): Advanced Applications and Accessibility +
        +
      • KeySlides (presentation creation and delivery)
      • +
      • KeyCode (text editor with syntax highlighting for programming)
      • +
      • Third-party app integration (Dropbox, Google Drive, OneDrive)
      • +
      • Braille input configuration (computer braille, contracted, literary)
      • +
      • Braille output settings (display mode, translation tables)
      • +
      • Device settings and preferences
      • +
      • Accessibility features (speech output, magnification, contrast)
      • +
      +
    • +
    • Phase 4 (P4_1–P4_3): Advanced File and Cloud Management
    • +
    • Phase 5 (P5_1–P5_4): Collaboration and Export Workflows
    • +
    • Phase 6 (P6_1–P6_3): App Ecosystem and Troubleshooting
    • +
    • Phase 7 (P7_1–P7_4): Automation and Customization
    • +
    • Phase 8 (P8_1–P8_5): Peripheral Integration (Bluetooth/USB devices, displays, audio/video)
    • +
    • Phase 9 (P9_1–P9_4): Security and Network Configuration
    • +
    • Phase 10 (P10_1–P10_3): Speech Engine Customization
    • +
    • Phase 11 (P11_1–P11_5): Maintenance and Support (firmware, diagnostics, warranty)
    • +
    • Phase 12 (P12_1–P12_4): Community and Online Resources
    • +
    + +

    Data Management and Artifacts:

    +
      +
    • Scores captured via PhaseScoreField (integer 0–4 typical)
    • +
    • Persisted to normalized schema via Database.insertAssessmentResults(int, int, java.lang.String[], int[])
    • +
    • JSON export: StudentDataFiles/<student>/Sessions/BrailleNote/BrailleNote-<sessionId>-<timestamp>.json
    • +
    • Phase-grouped time-series plots: plots/BrailleNote-<sessionId>-<date>-P<N>.png (12 phase groups)
    • +
    • Markdown and HTML reports with embedded plots and color-coded legends
    • +
    + +

    The shared JLineGraph visualizes recent session trends grouped by phase prefix. + Implements DateChangeListener and StudentChangeListener + for dynamic updates when global student/date selections change.

    See Also:
    @@ -228,7 +287,7 @@

    Constructor Details

  • BrailleNote

    -
    public BrailleNote(String studentName, +
    public BrailleNote(String studentName, LocalDate date, JLineGraph lineGraph)
    Create the BrailleNote page for a specific student and date.
    @@ -251,7 +310,7 @@

    Method Details

  • dateChanged

    -
    public void dateChanged(LocalDate newDate)
    +
    public void dateChanged(LocalDate newDate)
    Description copied from interface: DateChangeListener
    Called when the application date has been changed by the user.
    @@ -265,7 +324,7 @@

    dateChanged

  • studentChanged

    -
    public void studentChanged(String newStudent)
    +
    public void studentChanged(String newStudent)
    Description copied from interface: StudentChangeListener
    Called when the application selected student has changed.
    diff --git a/target/apidocs/com/studentgui/apppages/BrailleSense.html b/target/site/apidocs/com/studentgui/apppages/BrailleSense.html similarity index 95% rename from target/apidocs/com/studentgui/apppages/BrailleSense.html rename to target/site/apidocs/com/studentgui/apppages/BrailleSense.html index 9fd7e27..f4b1567 100644 --- a/target/apidocs/com/studentgui/apppages/BrailleSense.html +++ b/target/site/apidocs/com/studentgui/apppages/BrailleSense.html @@ -1,11 +1,11 @@ - + BrailleSense (Vision Skills Progression Tracker 1.0.0-beta API) - + @@ -104,18 +104,55 @@

    Class BrailleSense

    ImageObserver, MenuContainer, Serializable, Accessible

    -
    public class BrailleSense +
    public class BrailleSense extends JPanel
    -
    BrailleSense skills progression UI page. -

    - Presents a compact set of inputs keyed by part code (e.g. P1_1) and allows - saving those values into the canonical database schema. A shared - JLineGraph instance is used to visualize recent results. -

    +
    HIMS BrailleSense productivity device proficiency assessment page. + +

    Evaluates student competency with the HIMS BrailleSense family of refreshable braille + notetakers (BrailleSense Polaris, BrailleSense 6, etc.) across 52 skills organized into + 12 functional domains. The BrailleSense assessment structure mirrors BrailleNote + to allow cross-device skill comparison.

    + +

    Device Family Context: The BrailleSense is a portable braille notetaker with + refreshable braille display, perkins-style keyboard, and integrated productivity software. + It runs proprietary HIMS firmware and includes word processing, email, web browsing, + media playback, and educational applications.

    + +

    Assessment Phases (12 domains, 52 skills):

    +
      +
    • Phase 1: Device fundamentals (layout, setup, navigation, file management, core apps)
    • +
    • Phase 2: Productivity suite (calendar, email, web, calculator, word processor)
    • +
    • Phase 3: Advanced apps (presentations, code editor, third-party integration, braille I/O)
    • +
    • Phase 4: Cloud integration and advanced file management
    • +
    • Phase 5: Collaboration, export/import, printing, backup workflows
    • +
    • Phase 6: App installation, updates, troubleshooting
    • +
    • Phase 7: Automation (custom shortcuts, macros, scripting)
    • +
    • Phase 8: Peripheral connectivity (Bluetooth, USB, displays, audio/video)
    • +
    • Phase 9: Security, user accounts, parental controls, network settings
    • +
    • Phase 10: Speech customization (TTS settings, voice profiles, languages)
    • +
    • Phase 11: Device maintenance (firmware, diagnostics, logs, support, warranty)
    • +
    • Phase 12: Community resources (online help, forums, feedback channels)
    • +
    + +

    Data Management and Report Generation:

    +
      +
    • Scores captured via PhaseScoreField components (integer 0–4 typical)
    • +
    • Persisted to normalized schema via Database.insertAssessmentResults(int, int, java.lang.String[], int[])
    • +
    • JSON export: StudentDataFiles/<student>/Sessions/BrailleSense/BrailleSense-<sessionId>-<timestamp>.json
    • +
    • Phase-grouped time-series plots: plots/BrailleSense-<sessionId>-<date>-P<N>.png (12 phase groups)
    • +
    • Markdown and HTML reports with embedded plots and color-coded legends
    • +
    + +

    The shared JLineGraph visualizes recent session trends grouped by phase prefix. + This page operates on static student/date parameters and does not implement listener interfaces.

    See Also:
    @@ -206,7 +243,7 @@

    Constructor Details

  • BrailleSense

    -
    public BrailleSense(String studentName, +
    public BrailleSense(String studentName, LocalDate date, JLineGraph graph)
    Create a BrailleSense page bound to the provided student and date.
    diff --git a/target/apidocs/com/studentgui/apppages/CVI.html b/target/site/apidocs/com/studentgui/apppages/CVI.html similarity index 96% rename from target/apidocs/com/studentgui/apppages/CVI.html rename to target/site/apidocs/com/studentgui/apppages/CVI.html index 0e1de58..2e40620 100644 --- a/target/apidocs/com/studentgui/apppages/CVI.html +++ b/target/site/apidocs/com/studentgui/apppages/CVI.html @@ -1,11 +1,11 @@ - + CVI (Vision Skills Progression Tracker 1.0.0-beta API) - + @@ -104,18 +104,59 @@

    Class CVI

    ImageObserver, MenuContainer, Serializable, Accessible

  • -
    public class CVI +
    public class CVI extends JPanel
    -
    Cortical Visual Impairment (CVI) progression page. -

    - Presents a collection of named inputs for CVI-related observation scores - and supports saving and plotting recent sessions via the shared - JLineGraph component. -

    +
    Cortical Visual Impairment (CVI) assessment page. + +

    Provides a structured scoring interface for evaluating the 10 characteristic behaviors + associated with Cortical Visual Impairment as defined in the Roman-Lanzi CVI Range assessment + framework. Skills are organized into two functional clusters:

    + +
      +
    • Phase 1 (P1_1–P1_6): Primary CVI Characteristics +
        +
      • Color Preference: Preference for high-saturation colors (red, yellow)
      • +
      • Need for Movement: Improved visual attention with motion
      • +
      • Latency: Delayed visual response times
      • +
      • Field Preference: Asymmetric visual field usage patterns
      • +
      • Visual Complexity: Difficulty with cluttered/busy visual environments
      • +
      • Nonpurposeful Gaze: Reduced sustained visual fixation
      • +
      +
    • +
    • Phase 2 (P2_1–P2_4): Secondary/Environmental Characteristics +
        +
      • Distance Viewing: Reduced effectiveness at distance
      • +
      • Atypical Reflexes: Blink-to-threat, light reflex variations
      • +
      • Visual Novelty: Preference for familiar objects/environments
      • +
      • Visual Reach: Difficulty localizing and reaching toward objects
      • +
      +
    • +
    + +

    Scoring and Interpretation: Each characteristic is typically scored on a 0–10 scale + representing frequency/severity of the behavior. Higher scores may indicate greater impact + depending on the specific assessment protocol in use. Consult the Roman-Lanzi CVI Range manual + for standardized scoring guidelines.

    + +

    Data Management:

    +
      +
    • Scores captured via PhaseScoreField components with integer validation
    • +
    • Submit button persists to database via Database.insertAssessmentResults(int, int, java.lang.String[], int[])
    • +
    • Session JSON exported to StudentDataFiles/<student>/Sessions/CVI/CVI-<sessionId>-<timestamp>.json
    • +
    • Time-series plots generated per phase group and saved to plots/ directory
    • +
    • Markdown and HTML reports generated with embedded plots and color-coded legends
    • +
    + +

    The shared JLineGraph component visualizes trends across multiple sessions, + grouped by phase to separate primary and secondary characteristics. This page does not + implement listener interfaces as it operates on static student/date parameters.

    See Also:
    @@ -206,7 +247,7 @@

    Constructor Details

  • CVI

    -
    public CVI(String studentName, +
    public CVI(String studentName, LocalDate date, JLineGraph graph)
    Construct the CVI page bound to the selected student and session date.
    diff --git a/target/apidocs/com/studentgui/apppages/ContactLog.html b/target/site/apidocs/com/studentgui/apppages/ContactLog.html similarity index 95% rename from target/apidocs/com/studentgui/apppages/ContactLog.html rename to target/site/apidocs/com/studentgui/apppages/ContactLog.html index 37c9161..3aa4595 100644 --- a/target/apidocs/com/studentgui/apppages/ContactLog.html +++ b/target/site/apidocs/com/studentgui/apppages/ContactLog.html @@ -1,11 +1,11 @@ - + ContactLog (Vision Skills Progression Tracker 1.0.0-beta API) - + @@ -104,17 +104,52 @@

    Class ContactLog

    ImageObserver, MenuContainer, Serializable, Accessible

  • -
    public class ContactLog +
    public class ContactLog extends JPanel
    -
    Contact log page for storing freeform contact notes for a student. -

    - Provides a multi-line text area for notes and persists them to the - normalized database as session notes using the Database helper. -

    +
    Structured parent/guardian contact log with validation and freeform notes. + +

    Provides a comprehensive contact tracking form with structured fields for documenting + communications with parents, guardians, and family members. Unlike the freeform notes pages + (SessionNotes, Observations), this page captures both structured metadata + and narrative details to support later reporting and documentation requirements.

    + +

    Structured Fields:

    +
      +
    • Guardian Name: Full name of the parent/guardian contacted
    • +
    • Contact Method: Dropdown selection (Phone, Email, In Person, Other)
    • +
    • Phone Number: Contact phone number (validated format: 7-20 chars, digits/+/()-/space)
    • +
    • Email Address: Contact email (validated format: basic email regex pattern)
    • +
    • Contact Response: Brief summary of the guardian's response or concerns
    • +
    • Contact General: High-level topic or category of the contact (e.g., "Progress Update", "IEP Discussion")
    • +
    • Contact Specific: Specific items discussed or action points (e.g., "Discussed Braille materials order")
    • +
    • Notes: Multi-line freeform notes area for detailed narrative
    • +
    + +

    Validation and Error Handling:

    +
      +
    • Email validation: Triggers warning if Contact Method is "Email" and email field doesn't match ^[^@\s]+@[^@\s]+\.[^@\s]+$
    • +
    • Phone validation: Triggers warning if Contact Method is "Phone" and phone doesn't match ^[0-9+()\-\s]{7,20}$
    • +
    • Validation failures display warning dialogs and do not persist data until corrected
    • +
    + +

    Data Persistence:

    + + +

    No plots are generated (contact logs are non-quantitative). The shared JLineGraph component + is absent from this page's layout. This page does not implement listener interfaces and operates + on static student/date parameters.

    See Also:
    -
    @@ -205,7 +240,7 @@

    Constructor Details

  • ContactLog

    -
    public ContactLog(String studentName, +
    public ContactLog(String studentName, LocalDate date, JLineGraph graph)
    Construct a ContactLog page for the provided student and date.
    diff --git a/target/apidocs/com/studentgui/apppages/DigitalLiteracy.html b/target/site/apidocs/com/studentgui/apppages/DigitalLiteracy.html similarity index 94% rename from target/apidocs/com/studentgui/apppages/DigitalLiteracy.html rename to target/site/apidocs/com/studentgui/apppages/DigitalLiteracy.html index f2b9ffa..8ce16dd 100644 --- a/target/apidocs/com/studentgui/apppages/DigitalLiteracy.html +++ b/target/site/apidocs/com/studentgui/apppages/DigitalLiteracy.html @@ -1,11 +1,11 @@ - + DigitalLiteracy (Vision Skills Progression Tracker 1.0.0-beta API) - + @@ -104,19 +104,88 @@

    Class DigitalLiteracy

    DateChangeListener, StudentChangeListener, ImageObserver, MenuContainer, Serializable, Accessible

  • -
    public class DigitalLiteracy + -
    Digital literacy skills progression page UI. -

    - Presents a set of numeric input fields for digital literacy skills and - persists entries to the normalized database. A provided JLineGraph - instance is used to visualize recent assessment sessions. -

    +
    Digital literacy and computer skills assessment page. + +

    Evaluates foundational technology competencies required for academic and professional + success in digital environments. Covers 27 skills organized into 5 progressive competency domains:

    + +
      +
    • Phase 1 (P1_1–P1_9): Device Basics and Navigation +
        +
      • Powering devices on/off, accessibility feature activation (VoiceOver/TalkBack/Narrator)
      • +
      • Touch/mouse gestures for app launching and navigation
      • +
      • Home screen organization, icon identification, and app launching
      • +
      • Document creation, saving, and retrieval workflows
      • +
      • Online resource access (web portals, learning management systems)
      • +
      • Basic keyboarding (home row, touch typing fundamentals)
      • +
      • UI element interaction (buttons, menus, text fields, sliders)
      • +
      • System-level navigation (Control Center, App Switcher, Task Manager, Dock)
      • +
      +
    • +
    • Phase 2 (P2_1–P2_6): Word Processing Fundamentals +
        +
      • Creating, editing, and saving text documents
      • +
      • Reading and navigating documents using assistive technology or visual scanning
      • +
      • Menu bar and toolbar interaction for formatting and commands
      • +
      • Text selection, highlighting, copy/paste workflows
      • +
      • Image insertion and manipulation (copy, paste, resize, position)
      • +
      • Proofreading strategies and editing for clarity/correctness
      • +
      +
    • +
    • Phase 3 (P3_1–P3_3): Spreadsheet Fundamentals +
        +
      • Describing spreadsheet structure (rows, columns, cells, sheets)
      • +
      • Spreadsheet terminology (cell references, formulas, functions, ranges)
      • +
      • Data entry and editing (typing, autofill, formula entry)
      • +
      +
    • +
    • Phase 4 (P4_1–P4_5): Presentation Software +
        +
      • Presentation tool concepts (slides, layouts, templates)
      • +
      • Creating structured presentations (title, content, transitions)
      • +
      • Editing slides (text, formatting, reordering)
      • +
      • Presenting slides effectively (presenter view, navigation, notes)
      • +
      • Sharing presentations (export, cloud upload, email)
      • +
      +
    • +
    • Phase 5 (P5_1–P5_5): Digital Citizenship and Ethics +
        +
      • Acceptable Use Policies (school/workplace technology guidelines)
      • +
      • Digital citizenship principles (respectful communication, netiquette)
      • +
      • Internet safety (phishing, malware, safe browsing)
      • +
      • Copyright awareness (fair use, attribution, Creative Commons)
      • +
      • Plagiarism recognition and avoidance (paraphrasing, citations, originality)
      • +
      +
    • +
    + +

    Data Persistence and Report Generation:

    +
      +
    • Scores captured via PhaseScoreField (integer 0–4 typical)
    • +
    • Persisted to normalized schema via Database.insertAssessmentResults(int, int, java.lang.String[], int[])
    • +
    • JSON export: StudentDataFiles/<student>/Sessions/DigitalLiteracy/DigitalLiteracy-<sessionId>-<timestamp>.json
    • +
    • Phase-grouped time-series plots: plots/DigitalLiteracy-<sessionId>-<date>-P<N>.png (5 phase groups)
    • +
    • Markdown and HTML reports with embedded plots and color-coded legends
    • +
    + +

    The shared JLineGraph visualizes recent session trends grouped by phase prefix. + Implements DateChangeListener and StudentChangeListener + for dynamic updates when global selections change.

    + +

    Note: Skill codes and phases intentionally overlap with IOS to allow + cross-platform skill mapping. Some assessment items are device-agnostic and track the same + underlying competencies across iOS, Windows, macOS, and ChromeOS environments.

    See Also:
    @@ -227,7 +296,7 @@

    Constructor Details

  • DigitalLiteracy

    -
    public DigitalLiteracy(String studentName, +
    public DigitalLiteracy(String studentName, LocalDate date, JLineGraph lineGraph)
    Construct the Digital Literacy page for the given student and date.
    @@ -250,7 +319,7 @@

    Method Details

  • dateChanged

    -
    public void dateChanged(LocalDate newDate)
    +
    public void dateChanged(LocalDate newDate)
    Description copied from interface: DateChangeListener
    Called when the application date has been changed by the user.
    @@ -264,7 +333,7 @@

    dateChanged

  • studentChanged

    -
    public void studentChanged(String newStudent)
    +
    public void studentChanged(String newStudent)
    Description copied from interface: StudentChangeListener
    Called when the application selected student has changed.
    diff --git a/target/apidocs/com/studentgui/apppages/Homepage.html b/target/site/apidocs/com/studentgui/apppages/Homepage.html similarity index 96% rename from target/apidocs/com/studentgui/apppages/Homepage.html rename to target/site/apidocs/com/studentgui/apppages/Homepage.html index 4fee0ba..c2feee9 100644 --- a/target/apidocs/com/studentgui/apppages/Homepage.html +++ b/target/site/apidocs/com/studentgui/apppages/Homepage.html @@ -1,11 +1,11 @@ - + Homepage (Vision Skills Progression Tracker 1.0.0-beta API) - + @@ -92,7 +92,7 @@

    Class Homepage


  • -
    public class Homepage +
    public class Homepage extends Object
    Simple homepage panel with application overview/help text. @@ -138,7 +138,7 @@

    Method Details

  • create

    -
    public static JPanel create()
    +
    public static JPanel create()
    Create the homepage panel which contains a title and an overview/help text area.
    diff --git a/target/apidocs/com/studentgui/apppages/IOS.html b/target/site/apidocs/com/studentgui/apppages/IOS.html similarity index 95% rename from target/apidocs/com/studentgui/apppages/IOS.html rename to target/site/apidocs/com/studentgui/apppages/IOS.html index 974ddf1..13a9bd6 100644 --- a/target/apidocs/com/studentgui/apppages/IOS.html +++ b/target/site/apidocs/com/studentgui/apppages/IOS.html @@ -1,11 +1,11 @@ - + IOS (Vision Skills Progression Tracker 1.0.0-beta API) - + @@ -104,18 +104,81 @@

    Class IOS

    ImageObserver, MenuContainer, Serializable, Accessible

    -
    public class IOS +
    public class IOS extends JPanel
    -
    iOS / iPadOS skills progression page. -

    - Presents a map of device and app related skills keyed by part codes and - allows saving and plotting of recent assessment sessions using the shared - JLineGraph instance. -

    +
    iOS and iPadOS assistive technology proficiency assessment page. + +

    Provides structured evaluation of iOS/iPadOS device usage skills across + 41 competencies organized into 6 functional domains:

    + +
      +
    • Phase 1 (P1_1–P1_9): Device Basics and VoiceOver Fundamentals +
        +
      • Power management, VoiceOver activation/deactivation
      • +
      • Core gestures (tap, swipe, rotor) for icon navigation and interaction
      • +
      • Home screen management, document handling, keyboarding basics
      • +
      • Control Center, App Switcher, and system-level navigation
      • +
      +
    • +
    • Phase 2 (P2_1–P2_6): Word Processing and Document Creation +
        +
      • Creating, editing, and saving text documents
      • +
      • Reading and navigating within documents using VoiceOver
      • +
      • Menu bar interaction, text/image copy-paste workflows
      • +
      • Proofreading and editing strategies with assistive technology
      • +
      +
    • +
    • Phase 3 (P3_1–P3_5): Spreadsheet and Data Visualization +
        +
      • Spreadsheet concepts and terminology (rows, columns, cells, formulas)
      • +
      • Data entry, editing, and spreadsheet navigation with VoiceOver
      • +
      • Creating and interpreting charts/graphs from data
      • +
      +
    • +
    • Phase 4 (P4_1–P4_5): Presentation Software +
        +
      • Creating and structuring presentations with accessible workflows
      • +
      • Editing slides, adding multimedia content (images, audio)
      • +
      • Presenting slides effectively using assistive technology
      • +
      • Sharing and exporting presentations
      • +
      +
    • +
    • Phase 5 (P5_1–P5_7): Digital Citizenship and Online Safety +
        +
      • Acceptable Use Policies, digital citizenship principles
      • +
      • Online safety, privacy awareness, copyright/plagiarism concepts
      • +
      • Recognizing and responding to cyberbullying
      • +
      +
    • +
    • Phase 6 (P6_1–P6_11): Device Management and Connectivity +
        +
      • App installation, updates, deletion, storage management
      • +
      • Accessibility settings configuration and customization
      • +
      • Screen Time controls, Parental Controls
      • +
      • Connectivity features: Bluetooth, Wi-Fi, AirDrop, Personal Hotspot
      • +
      +
    • +
    + +

    Data Management and Artifacts:

    +
      +
    • Scores captured via PhaseScoreField components (typically 0–4 integer range)
    • +
    • Persisted to normalized schema using Database.insertAssessmentResults(int, int, java.lang.String[], int[])
    • +
    • JSON session export: StudentDataFiles/<student>/Sessions/iOS/iOS-<sessionId>-<timestamp>.json
    • +
    • Phase-grouped time-series PNG plots saved to plots/ directory
    • +
    • Markdown and HTML reports generated with embedded plots and color-coded legends
    • +
    + +

    The shared JLineGraph visualizes recent session trends grouped by phase prefix + to maintain chart readability. This page operates on static student/date parameters and + does not implement listener interfaces for dynamic updates.

    See Also:
    @@ -206,7 +269,7 @@

    Constructor Details

  • IOS

    -
    public IOS(String studentName, +
    public IOS(String studentName, LocalDate date, JLineGraph graph)
    Construct the iOS page for the given student and date.
    diff --git a/target/apidocs/com/studentgui/apppages/InstructionalMaterials.html b/target/site/apidocs/com/studentgui/apppages/InstructionalMaterials.html similarity index 98% rename from target/apidocs/com/studentgui/apppages/InstructionalMaterials.html rename to target/site/apidocs/com/studentgui/apppages/InstructionalMaterials.html index 1812eda..c95f48b 100644 --- a/target/apidocs/com/studentgui/apppages/InstructionalMaterials.html +++ b/target/site/apidocs/com/studentgui/apppages/InstructionalMaterials.html @@ -1,11 +1,11 @@ - + InstructionalMaterials (Vision Skills Progression Tracker 1.0.0-beta API) - + @@ -104,13 +104,33 @@

    Class InstructionalMateri
    ImageObserver, MenuContainer, Serializable, Accessible


  • -
    public class InstructionalMaterials +
    public class InstructionalMaterials extends JPanel
    -
    Instructional materials viewer panel. -

    - Displays a read-only listing or links to instructional resources. This is - primarily a static help/documentation view and does not persist data. -

    +
    Instructional materials and resources reference page. + +

    Provides a simple placeholder panel for displaying links, documentation, or references + to external instructional resources. This is a static informational view without data + persistence or assessment functionality.

    + +

    Current Implementation:

    +
      +
    • Read-only text area with placeholder content
    • +
    • Refresh button (currently logs action but performs no operation)
    • +
    • No database persistence or session tracking
    • +
    • Intended for future expansion with resource links, PDF viewers, or material management UI
    • +
    + +

    Potential Future Enhancements:

    +
      +
    • Dynamic listing of student-specific materials from StudentDataFiles/<student>/InstructionalMaterials/
    • +
    • PDF preview integration for viewing documents inline
    • +
    • File upload and organization capabilities
    • +
    • Links to online resources (curriculum guides, training videos, vendor documentation)
    • +
    • Material assignment workflow (track which materials were provided to student/family)
    • +
    + +

    This page does not implement listener interfaces and does not interact with the database. + It serves as a navigation target and placeholder for future resource management features.

    See Also:
    @@ -203,7 +223,7 @@

    Constructor Details

  • InstructionalMaterials

    -
    public InstructionalMaterials()
    +
    Create the Instructional Materials page.
  • diff --git a/target/apidocs/com/studentgui/apppages/JLineGraph.html b/target/site/apidocs/com/studentgui/apppages/JLineGraph.html similarity index 91% rename from target/apidocs/com/studentgui/apppages/JLineGraph.html rename to target/site/apidocs/com/studentgui/apppages/JLineGraph.html index a59874e..558841c 100644 --- a/target/apidocs/com/studentgui/apppages/JLineGraph.html +++ b/target/site/apidocs/com/studentgui/apppages/JLineGraph.html @@ -1,11 +1,11 @@ - + JLineGraph (Vision Skills Progression Tracker 1.0.0-beta API) - + @@ -104,35 +104,83 @@

    Class JLineGraph

    SettingsChangeListener, ImageObserver, MenuContainer, Serializable, Accessible

    -
    public class JLineGraph +
    public class JLineGraph extends JPanel implements SettingsChangeListener
    -
    Lightweight line chart component used across pages to display recent - assessment sessions. Wraps a JFreeChart XY plot and exposes a small set - of convenience update methods used by the application pages: +
    Reusable JFreeChart-based line chart component for visualizing student assessment progress. + +

    This component is shared across all assessment pages (Braille, Abacus, iOS, ScreenReader, etc.) + to display time-series data showing skill progression over multiple sessions. It supports three + primary visualization modes:

    + + + +

    Visual Design and Rendering:

    - Important implementation notes: +

    Typical Workflow for Assessment Pages:

    +
      +
    1. Page fetches recent sessions from database via Database.fetchLatestAssessmentResultsWithDates(java.lang.String, java.lang.String, int)
    2. +
    3. Page calls updateWithGroupedDataByDate(java.util.List, java.util.List, String[], String[]) to populate chart
    4. +
    5. On submit, page calls saveGroupedCharts(java.nio.file.Path, String, int, int) to export PNG images
    6. +
    7. Page generates Markdown/HTML reports linking to the exported plots
    8. +
    + +

    Export and Persistence:

      -
    • Rendering jitter: a small visual jitter of +/- JITTER_AMPLITUDE - is applied to plotted points via addJitter(double) to help - reveal overlapping points. This is a display-only transformation - and does not modify persisted session values.
    • -
    • Background bands: the component draws horizontal colored bands to - indicate score ranges; the bands use the ranges: red = -0.25..0.5, - orange = 0.5..1.5, orange = 1.5..2.5, yellow = 2.5..3.5, green = - 3.5..4.5. The Y-axis range is set to -0.25 .. 4.25 by default.
    • -
    • Grouped charts and time-series charts share the same band drawing - helper addHorizontalBands(org.jfree.chart.plot.XYPlot, double, double)
    • -
    +
  • saveGroupedCharts(java.nio.file.Path, String, int, int) - Exports each phase group as a separate PNG file
  • +
  • saveChart(java.nio.file.Path, int, int) - Exports the single main chart (when not in grouped mode)
  • +
  • Returns Map<groupName, filePath> for use in report generation
  • + + +

    Accessibility:

    +
      +
    • ChartPanel accessible name set to "Skill progression chart"
    • +
    • Tooltips enabled showing coordinate values on hover
    • +
    • Keyboard navigation supported through JFreeChart's default ChartPanel behavior
    • +
    + +

    Settings Integration: Implements SettingsChangeListener to respond + to jitter configuration changes at runtime without requiring application restart.

    See Also:
    -
    @@ -329,14 +377,14 @@

    Field Details

  • PALETTE_HEX

    -
    public static final String[] PALETTE_HEX
    +
    public static final String[] PALETTE_HEX
    Public color palette (hex) for HTML legends and consistency across pages.
  • PALETTE

    -
    public static final Color[] PALETTE
    +
    public static final Color[] PALETTE
    Public color palette as AWT Color objects for chart rendering.
  • @@ -351,7 +399,7 @@

    Constructor Details

  • JLineGraph

    -
    public JLineGraph()
    +
    public JLineGraph()
    Create a new JLineGraph with default styling and an empty dataset.
  • @@ -366,7 +414,7 @@

    Method Details

  • settingsChanged

    -
    public void settingsChanged()
    +
    public void settingsChanged()
    Description copied from interface: SettingsChangeListener
    Invoked when application settings have been changed and persisted. Implementations should read the desired values from the Settings @@ -380,7 +428,7 @@

    settingsChanged

  • setJitterEnabled

    -
    public void setJitterEnabled(boolean enabled)
    +
    public void setJitterEnabled(boolean enabled)
    Enable or disable rendering jitter at runtime.
    Parameters:
    @@ -391,7 +439,7 @@

    setJitterEnabled

  • isJitterEnabled

    -
    public boolean isJitterEnabled()
    +
    public boolean isJitterEnabled()
    Query whether rendering jitter is currently enabled.
    Returns:
    @@ -402,7 +450,7 @@

    isJitterEnabled

  • setJitterDeterministic

    -
    public void setJitterDeterministic(boolean deterministic)
    +
    public void setJitterDeterministic(boolean deterministic)
    Enable/disable deterministic (seeded) jitter. When enabled, jitter will be generated from a java.util.Random seeded with jitterSeed (or 0 when seed is null).
    @@ -415,7 +463,7 @@

    setJitterDeterministic

  • isJitterDeterministic

    -
    public boolean isJitterDeterministic()
    +
    public boolean isJitterDeterministic()
    Query whether deterministic jitter is enabled.
    Returns:
    @@ -426,7 +474,7 @@

    isJitterDeterministic

  • setJitterSeed

    -
    public void setJitterSeed(Long seed)
    +
    public void setJitterSeed(Long seed)
    Set the seed used when deterministic jitter is enabled. Pass null to clear the seed (will use 0 when a deterministic RNG is created).
    @@ -438,7 +486,7 @@

    setJitterSeed

  • getJitterSeed

    -
    public Long getJitterSeed()
    +
    public Long getJitterSeed()
    Return the currently configured jitter seed or null when unset.
    Returns:
    @@ -449,7 +497,7 @@

    getJitterSeed

  • updateWithData

    -
    public void updateWithData(List<List<Integer>> allSkillValues)
    +
    public void updateWithData(List<List<Integer>> allSkillValues)
    Replace the current dataset with the provided list of skill value series. Each inner list represents a single session and must contain NUMBER_OF_SKILLS entries.
    @@ -463,7 +511,7 @@

    updateWithData

  • updateWithGroupedData

    -
    public void updateWithGroupedData(List<List<Integer>> allSkillValues, +
    public void updateWithGroupedData(List<List<Integer>> allSkillValues, String[] partCodes)
    Update the component with grouped plots. Each group is determined by the prefix of the part code (e.g. 'P1' from 'P1_1'). For each group we render @@ -478,7 +526,7 @@

    updateWithGroupedData

  • updateWithGroupedDataByDate

    -
    public void updateWithGroupedDataByDate(List<LocalDate> dates, +
    public void updateWithGroupedDataByDate(List<LocalDate> dates, List<List<Integer>> rows, String[] partCodes)
    Plot grouped data over time. Dates are used as the X axis (oldest first). @@ -496,7 +544,7 @@

    updateWithGroupedDataByDate

  • updateWithGroupedDataByDate

    -
    public void updateWithGroupedDataByDate(List<LocalDate> dates, +
    public void updateWithGroupedDataByDate(List<LocalDate> dates, List<List<Integer>> rows, String[] partCodes, String[] partLabels)
    @@ -517,7 +565,7 @@

    updateWithGroupedDataByDate

  • saveGroupedCharts

    -
    public Map<String,Path> saveGroupedCharts(Path dir, +
    public Map<String,Path> saveGroupedCharts(Path dir, String baseName, int width, int heightPerGroup) @@ -542,7 +590,7 @@

    saveGroupedCharts

  • showEmptyGrouped

    -
    public void showEmptyGrouped(String[] partCodes)
    +
    public void showEmptyGrouped(String[] partCodes)
    Show an empty grouped chart using the provided part codes. This will render one row of zeros sized to the number of parts so the UI shows grouped axes and placeholders even when no session data exists yet.
    @@ -555,7 +603,7 @@

    showEmptyGrouped

  • saveChart

    -
    public void saveChart(Path outputPath, +
    public void saveChart(Path outputPath, int width, int height) throws IOException
    diff --git a/target/apidocs/com/studentgui/apppages/Keyboarding.html b/target/site/apidocs/com/studentgui/apppages/Keyboarding.html similarity index 96% rename from target/apidocs/com/studentgui/apppages/Keyboarding.html rename to target/site/apidocs/com/studentgui/apppages/Keyboarding.html index ffa7668..a554f48 100644 --- a/target/apidocs/com/studentgui/apppages/Keyboarding.html +++ b/target/site/apidocs/com/studentgui/apppages/Keyboarding.html @@ -1,11 +1,11 @@ - + Keyboarding (Vision Skills Progression Tracker 1.0.0-beta API) - + @@ -104,16 +104,45 @@

    Class Keyboarding

    DateChangeListener, StudentChangeListener, ImageObserver, MenuContainer, Serializable, Accessible

  • -
    public class Keyboarding + -
    Keyboarding skills page. Captures program/topic/speed/accuracy results and - persists them to a dedicated keyboarding result table via the - Database helper.
    +
    Touch-typing and keyboarding skills assessment page. + +

    Unlike other assessment pages that use phase-score grids, this page captures + structured performance metrics for keyboarding practice sessions:

    + +
      +
    • Program: Name of the typing curriculum or software (e.g., TypingClub, KeyBlaze, Braille2000)
    • +
    • Topic: Specific lesson, module, or exercise completed (e.g., "Home Row Mastery", "Lesson 12")
    • +
    • Speed (WPM): Words per minute achieved during the timed exercise
    • +
    • Accuracy (%): Percentage of characters typed correctly
    • +
    + +

    Data Persistence:

    + + +

    Validation and Error Handling:

    +
      +
    • Speed and Accuracy fields must contain whole numbers (non-negative integers)
    • +
    • Empty speed/accuracy fields default to 0 for leniency
    • +
    • Invalid input triggers error dialogs and field focus for correction
    • +
    + +

    The shared JLineGraph component is present for UI consistency but is not populated + with keyboarding data (keyboarding does not use assessment parts). Implements + DateChangeListener and StudentChangeListener + for title updates when global selections change.

    See Also:
    -
    @@ -224,7 +253,7 @@

    Constructor Details

  • Keyboarding

    -
    public Keyboarding(String studentName, +
    public Keyboarding(String studentName, LocalDate date, JLineGraph lineGraph)
    Construct the Keyboarding page for a specific student and session date.
    @@ -247,7 +276,7 @@

    Method Details

  • dateChanged

    -
    public void dateChanged(LocalDate newDate)
    +
    public void dateChanged(LocalDate newDate)
    Description copied from interface: DateChangeListener
    Called when the application date has been changed by the user.
    @@ -261,7 +290,7 @@

    dateChanged

  • studentChanged

    -
    public void studentChanged(String newStudent)
    +
    public void studentChanged(String newStudent)
    Description copied from interface: StudentChangeListener
    Called when the application selected student has changed.
    diff --git a/target/apidocs/com/studentgui/apppages/Observations.html b/target/site/apidocs/com/studentgui/apppages/Observations.html similarity index 97% rename from target/apidocs/com/studentgui/apppages/Observations.html rename to target/site/apidocs/com/studentgui/apppages/Observations.html index 43d8ca3..62af570 100644 --- a/target/apidocs/com/studentgui/apppages/Observations.html +++ b/target/site/apidocs/com/studentgui/apppages/Observations.html @@ -1,11 +1,11 @@ - + Observations (Vision Skills Progression Tracker 1.0.0-beta API) - + @@ -104,13 +104,39 @@

    Class Observations

    ImageObserver, MenuContainer, Serializable, Accessible

    -
    public class Observations +
    public class Observations extends JPanel
    -
    Observations page for recording freeform observational notes.
    +
    Observational notes page for documenting unstructured student behaviors and progress. + +

    Similar to SessionNotes but intended for ongoing observational records rather than + post-session reflections. Provides a multi-line text area for educators to capture qualitative + observations throughout or across multiple sessions.

    + +

    Typical Use Cases:

    +
      +
    • Recording specific skill demonstrations observed in real-time (e.g., "Student independently located Braille cell for letter 'G' after 2 attempts")
    • +
    • Documenting spontaneous behaviors or breakthroughs (e.g., "First time student used VoiceOver gestures without prompting")
    • +
    • Noting patterns over time (e.g., "Third session this week where student requested breaks during Abacus work")
    • +
    • Functional vision assessments and CVI-related observations
    • +
    + +

    Data Persistence:

    +
      +
    • Notes saved via Database.saveSessionNotes(int, java.lang.String) to ProgressSession.notes column
    • +
    • Associated with an Observations progress type for categorization
    • +
    • Dummy assessment result (code="OBS_NOTE", score=0) inserted to satisfy schema constraints
    • +
    • JSON export: StudentDataFiles/<student>/Sessions/Observations/Observations-<sessionId>-<timestamp>.json
    • +
    + +

    No plots or quantitative reports are generated. This page does not implement listener interfaces + and operates on static student/date parameters set at construction time.

    See Also:
    -
    @@ -200,7 +226,7 @@

    Constructor Details

  • Observations

    -
    public Observations(String studentName, +
    public Observations(String studentName, LocalDate date)
    Create an Observations page for the given student and date.
    diff --git a/target/apidocs/com/studentgui/apppages/ScreenReader.html b/target/site/apidocs/com/studentgui/apppages/ScreenReader.html similarity index 94% rename from target/apidocs/com/studentgui/apppages/ScreenReader.html rename to target/site/apidocs/com/studentgui/apppages/ScreenReader.html index d7dcffd..c574ccf 100644 --- a/target/apidocs/com/studentgui/apppages/ScreenReader.html +++ b/target/site/apidocs/com/studentgui/apppages/ScreenReader.html @@ -1,11 +1,11 @@ - + ScreenReader (Vision Skills Progression Tracker 1.0.0-beta API) - + @@ -104,19 +104,81 @@

    Class ScreenReader

    DateChangeListener, StudentChangeListener, ImageObserver, MenuContainer, Serializable, Accessible

    -
    public class ScreenReader + -
    ScreenReader skills progression page. +
    Screen reader proficiency assessment page for desktop/laptop environments. - Displays a form of numeric fields representing screen reader skill codes - and provides persistence of those values to the canonical database. A - supplied JLineGraph is used to render - recent results below the form.
    +

    Evaluates student competency with screen reading software (JAWS, NVDA, Narrator, + VoiceOver macOS) across 28 standardized skills organized into 4 progressive competency phases:

    + +
      +
    • Phase 1 (P1_1–P1_6): Fundamental Navigation and Interaction +
        +
      • Basic keyboard navigation (Tab, arrow keys, application switching)
      • +
      • Reading and interpreting control labels and text content
      • +
      • Activating controls (buttons, links, checkboxes) via keyboard
      • +
      • Form entry (text fields, combo boxes, radio buttons)
      • +
      • Table navigation (row/column movement, header announcement)
      • +
      • Heading navigation (H key, heading list, semantic structure)
      • +
      +
    • +
    • Phase 2 (P2_1–P2_4): Web and Document Element Navigation +
        +
      • Link navigation and link list usage
      • +
      • List navigation (ordered, unordered, nested lists)
      • +
      • Image handling (alt text, long descriptions, graphics navigation)
      • +
      • Annotation and metadata awareness (ARIA labels, landmarks)
      • +
      +
    • +
    • Phase 3 (P3_1–P3_11): Advanced Document Structures and Customization +
        +
      • Document structure navigation (sections, articles, landmarks)
      • +
      • Style and formatting awareness (bold, italic, font changes)
      • +
      • Advanced table navigation (complex tables, merged cells, formulas)
      • +
      • Chart and graph interpretation with screen reader feedback
      • +
      • Advanced keyboard shortcuts and quick navigation commands
      • +
      • Scripting usage (JAWS scripts, NVDA add-ons)
      • +
      • Third-party application integration (Office, Adobe, IDEs)
      • +
      • Multimedia content handling (audio descriptions, video captions)
      • +
      • Braille display usage and synchronization
      • +
      • Braille table switching (Grade 1, Grade 2, computer braille)
      • +
      • Configuration and customization (speech rate, verbosity, sounds)
      • +
      +
    • +
    • Phase 4 (P4_1–P4_7): Efficiency, Troubleshooting, and Integration +
        +
      • Performance optimization (adjusting verbosity, quick navigation mastery)
      • +
      • Error recovery strategies (finding lost focus, restarting speech)
      • +
      • Integration across multiple assistive technologies (magnification, braille, OCR)
      • +
      • Accessibility API awareness (UI Automation, MSAA, IAccessible2)
      • +
      • Settings management (profiles, application-specific configurations)
      • +
      • Profile creation and switching for different workflows/applications
      • +
      • Accessing vendor support resources and community forums
      • +
      +
    • +
    + +

    Data Persistence and Report Generation:

    +
      +
    • Scores captured via PhaseScoreField components (integer 0–4 typical)
    • +
    • Persisted to normalized schema via Database.insertAssessmentResults(int, int, java.lang.String[], int[])
    • +
    • JSON export: StudentDataFiles/<student>/Sessions/ScreenReader/ScreenReader-<sessionId>-<timestamp>.json
    • +
    • Phase-grouped time-series PNG plots: plots/ScreenReader-<sessionId>-<date>-P<N>.png (4 phase groups)
    • +
    • Markdown report: reports/ScreenReader-<sessionId>-<date>.md with relative image links
    • +
    • HTML report: reports/ScreenReader-<sessionId>-<date>.html with inline styles and legends
    • +
    + +

    The shared JLineGraph visualizes recent session trends with phase-based grouping. + Implements DateChangeListener and StudentChangeListener + for dynamic refresh when global selections change.

    See Also:
    @@ -227,7 +289,7 @@

    Constructor Details

  • ScreenReader

    -
    public ScreenReader(String studentName, +
    public ScreenReader(String studentName, LocalDate date, JLineGraph lineGraph)
    Construct a ScreenReader page bound to a student and date. @@ -251,7 +313,7 @@

    Method Details

  • dateChanged

    -
    public void dateChanged(LocalDate newDate)
    +
    public void dateChanged(LocalDate newDate)
    Description copied from interface: DateChangeListener
    Called when the application date has been changed by the user.
    @@ -265,7 +327,7 @@

    dateChanged

  • studentChanged

    -
    public void studentChanged(String newStudent)
    +
    public void studentChanged(String newStudent)
    Description copied from interface: StudentChangeListener
    Called when the application selected student has changed.
    diff --git a/target/apidocs/com/studentgui/apppages/SessionNotes.html b/target/site/apidocs/com/studentgui/apppages/SessionNotes.html similarity index 97% rename from target/apidocs/com/studentgui/apppages/SessionNotes.html rename to target/site/apidocs/com/studentgui/apppages/SessionNotes.html index cea18e0..439b35a 100644 --- a/target/apidocs/com/studentgui/apppages/SessionNotes.html +++ b/target/site/apidocs/com/studentgui/apppages/SessionNotes.html @@ -1,11 +1,11 @@ - + SessionNotes (Vision Skills Progression Tracker 1.0.0-beta API) - + @@ -104,13 +104,40 @@

    Class SessionNotes

    ImageObserver, MenuContainer, Serializable, Accessible

    -
    public class SessionNotes +
    public class SessionNotes extends JPanel
    -
    Session notes editor page.
    +
    Freeform session notes editor for general observations and reflections. + +

    Provides a simple multi-line text area for educators to record unstructured notes + about a student session. This complements the structured assessment pages (Braille, Abacus, etc.) + by allowing qualitative observations, anecdotal records, and contextual details that don't + fit into numeric scoring fields.

    + +

    Typical Use Cases:

    +
      +
    • Recording behavioral observations (e.g., "Student showed increased frustration with Nemeth fractions today")
    • +
    • Documenting environmental factors affecting performance (e.g., "Noisy classroom due to construction")
    • +
    • Noting equipment issues or accommodations used (e.g., "Switched to Braille Sense due to BrailleNote malfunction")
    • +
    • General reflections or instructional notes for future reference
    • +
    + +

    Data Storage:

    +
      +
    • Notes persisted via Database.saveSessionNotes(int, java.lang.String) to ProgressSession.notes column
    • +
    • Associated with a SessionNotes progress type and session ID for consistent querying
    • +
    • JSON export: StudentDataFiles/<student>/Sessions/SessionNotes/SessionNotes-<sessionId>-<timestamp>.json
    • +
    • No plots or reports generated (text-only data)
    • +
    + +

    The shared JLineGraph component is present for UI layout consistency but remains + empty (session notes are not quantitative data). This page does not implement listener interfaces + as it operates on static student/date parameters provided at construction time.

    See Also:
    -
    @@ -201,7 +228,7 @@

    Constructor Details

  • SessionNotes

    -
    public SessionNotes(String studentName, +
    public SessionNotes(String studentName, LocalDate date, JLineGraph graph)
    Create a SessionNotes page for the provided student and date. diff --git a/target/apidocs/com/studentgui/apppages/class-use/Abacus.html b/target/site/apidocs/com/studentgui/apppages/class-use/Abacus.html similarity index 96% rename from target/apidocs/com/studentgui/apppages/class-use/Abacus.html rename to target/site/apidocs/com/studentgui/apppages/class-use/Abacus.html index d8d6d9c..e64c656 100644 --- a/target/apidocs/com/studentgui/apppages/class-use/Abacus.html +++ b/target/site/apidocs/com/studentgui/apppages/class-use/Abacus.html @@ -1,11 +1,11 @@ - + Uses of Class com.studentgui.apppages.Abacus (Vision Skills Progression Tracker 1.0.0-beta API) - + diff --git a/target/apidocs/com/studentgui/apppages/class-use/Braille.html b/target/site/apidocs/com/studentgui/apppages/class-use/Braille.html similarity index 96% rename from target/apidocs/com/studentgui/apppages/class-use/Braille.html rename to target/site/apidocs/com/studentgui/apppages/class-use/Braille.html index 4b4aaa4..da10937 100644 --- a/target/apidocs/com/studentgui/apppages/class-use/Braille.html +++ b/target/site/apidocs/com/studentgui/apppages/class-use/Braille.html @@ -1,11 +1,11 @@ - + Uses of Class com.studentgui.apppages.Braille (Vision Skills Progression Tracker 1.0.0-beta API) - + diff --git a/target/apidocs/com/studentgui/apppages/class-use/BrailleNote.html b/target/site/apidocs/com/studentgui/apppages/class-use/BrailleNote.html similarity index 96% rename from target/apidocs/com/studentgui/apppages/class-use/BrailleNote.html rename to target/site/apidocs/com/studentgui/apppages/class-use/BrailleNote.html index 71a7a76..5ae66ab 100644 --- a/target/apidocs/com/studentgui/apppages/class-use/BrailleNote.html +++ b/target/site/apidocs/com/studentgui/apppages/class-use/BrailleNote.html @@ -1,11 +1,11 @@ - + Uses of Class com.studentgui.apppages.BrailleNote (Vision Skills Progression Tracker 1.0.0-beta API) - + diff --git a/target/apidocs/com/studentgui/apppages/class-use/BrailleSense.html b/target/site/apidocs/com/studentgui/apppages/class-use/BrailleSense.html similarity index 96% rename from target/apidocs/com/studentgui/apppages/class-use/BrailleSense.html rename to target/site/apidocs/com/studentgui/apppages/class-use/BrailleSense.html index b4e2a95..2fe9246 100644 --- a/target/apidocs/com/studentgui/apppages/class-use/BrailleSense.html +++ b/target/site/apidocs/com/studentgui/apppages/class-use/BrailleSense.html @@ -1,11 +1,11 @@ - + Uses of Class com.studentgui.apppages.BrailleSense (Vision Skills Progression Tracker 1.0.0-beta API) - + diff --git a/target/apidocs/com/studentgui/apppages/class-use/CVI.html b/target/site/apidocs/com/studentgui/apppages/class-use/CVI.html similarity index 96% rename from target/apidocs/com/studentgui/apppages/class-use/CVI.html rename to target/site/apidocs/com/studentgui/apppages/class-use/CVI.html index 68d73bc..a07170a 100644 --- a/target/apidocs/com/studentgui/apppages/class-use/CVI.html +++ b/target/site/apidocs/com/studentgui/apppages/class-use/CVI.html @@ -1,11 +1,11 @@ - + Uses of Class com.studentgui.apppages.CVI (Vision Skills Progression Tracker 1.0.0-beta API) - + diff --git a/target/apidocs/com/studentgui/apppages/class-use/ContactLog.html b/target/site/apidocs/com/studentgui/apppages/class-use/ContactLog.html similarity index 96% rename from target/apidocs/com/studentgui/apppages/class-use/ContactLog.html rename to target/site/apidocs/com/studentgui/apppages/class-use/ContactLog.html index 336b37d..6e4ac0e 100644 --- a/target/apidocs/com/studentgui/apppages/class-use/ContactLog.html +++ b/target/site/apidocs/com/studentgui/apppages/class-use/ContactLog.html @@ -1,11 +1,11 @@ - + Uses of Class com.studentgui.apppages.ContactLog (Vision Skills Progression Tracker 1.0.0-beta API) - + diff --git a/target/apidocs/com/studentgui/apppages/class-use/DigitalLiteracy.html b/target/site/apidocs/com/studentgui/apppages/class-use/DigitalLiteracy.html similarity index 96% rename from target/apidocs/com/studentgui/apppages/class-use/DigitalLiteracy.html rename to target/site/apidocs/com/studentgui/apppages/class-use/DigitalLiteracy.html index 8845a80..068a0dc 100644 --- a/target/apidocs/com/studentgui/apppages/class-use/DigitalLiteracy.html +++ b/target/site/apidocs/com/studentgui/apppages/class-use/DigitalLiteracy.html @@ -1,11 +1,11 @@ - + Uses of Class com.studentgui.apppages.DigitalLiteracy (Vision Skills Progression Tracker 1.0.0-beta API) - + diff --git a/target/apidocs/com/studentgui/apppages/class-use/Homepage.html b/target/site/apidocs/com/studentgui/apppages/class-use/Homepage.html similarity index 96% rename from target/apidocs/com/studentgui/apppages/class-use/Homepage.html rename to target/site/apidocs/com/studentgui/apppages/class-use/Homepage.html index ba4f846..d373ad6 100644 --- a/target/apidocs/com/studentgui/apppages/class-use/Homepage.html +++ b/target/site/apidocs/com/studentgui/apppages/class-use/Homepage.html @@ -1,11 +1,11 @@ - + Uses of Class com.studentgui.apppages.Homepage (Vision Skills Progression Tracker 1.0.0-beta API) - + diff --git a/target/apidocs/com/studentgui/apppages/class-use/IOS.html b/target/site/apidocs/com/studentgui/apppages/class-use/IOS.html similarity index 96% rename from target/apidocs/com/studentgui/apppages/class-use/IOS.html rename to target/site/apidocs/com/studentgui/apppages/class-use/IOS.html index 32c9c67..1de01c4 100644 --- a/target/apidocs/com/studentgui/apppages/class-use/IOS.html +++ b/target/site/apidocs/com/studentgui/apppages/class-use/IOS.html @@ -1,11 +1,11 @@ - + Uses of Class com.studentgui.apppages.IOS (Vision Skills Progression Tracker 1.0.0-beta API) - + diff --git a/target/apidocs/com/studentgui/apppages/class-use/InstructionalMaterials.html b/target/site/apidocs/com/studentgui/apppages/class-use/InstructionalMaterials.html similarity index 96% rename from target/apidocs/com/studentgui/apppages/class-use/InstructionalMaterials.html rename to target/site/apidocs/com/studentgui/apppages/class-use/InstructionalMaterials.html index 8a7ee11..a5a9594 100644 --- a/target/apidocs/com/studentgui/apppages/class-use/InstructionalMaterials.html +++ b/target/site/apidocs/com/studentgui/apppages/class-use/InstructionalMaterials.html @@ -1,11 +1,11 @@ - + Uses of Class com.studentgui.apppages.InstructionalMaterials (Vision Skills Progression Tracker 1.0.0-beta API) - + diff --git a/target/apidocs/com/studentgui/apppages/class-use/JLineGraph.html b/target/site/apidocs/com/studentgui/apppages/class-use/JLineGraph.html similarity index 99% rename from target/apidocs/com/studentgui/apppages/class-use/JLineGraph.html rename to target/site/apidocs/com/studentgui/apppages/class-use/JLineGraph.html index 5a00f70..f42ee5f 100644 --- a/target/apidocs/com/studentgui/apppages/class-use/JLineGraph.html +++ b/target/site/apidocs/com/studentgui/apppages/class-use/JLineGraph.html @@ -1,11 +1,11 @@ - + Uses of Class com.studentgui.apppages.JLineGraph (Vision Skills Progression Tracker 1.0.0-beta API) - + diff --git a/target/apidocs/com/studentgui/apppages/class-use/Keyboarding.html b/target/site/apidocs/com/studentgui/apppages/class-use/Keyboarding.html similarity index 96% rename from target/apidocs/com/studentgui/apppages/class-use/Keyboarding.html rename to target/site/apidocs/com/studentgui/apppages/class-use/Keyboarding.html index afcbecd..e61c1d3 100644 --- a/target/apidocs/com/studentgui/apppages/class-use/Keyboarding.html +++ b/target/site/apidocs/com/studentgui/apppages/class-use/Keyboarding.html @@ -1,11 +1,11 @@ - + Uses of Class com.studentgui.apppages.Keyboarding (Vision Skills Progression Tracker 1.0.0-beta API) - + diff --git a/target/apidocs/com/studentgui/apppages/class-use/Observations.html b/target/site/apidocs/com/studentgui/apppages/class-use/Observations.html similarity index 96% rename from target/apidocs/com/studentgui/apppages/class-use/Observations.html rename to target/site/apidocs/com/studentgui/apppages/class-use/Observations.html index ee4e1f4..d9afb73 100644 --- a/target/apidocs/com/studentgui/apppages/class-use/Observations.html +++ b/target/site/apidocs/com/studentgui/apppages/class-use/Observations.html @@ -1,11 +1,11 @@ - + Uses of Class com.studentgui.apppages.Observations (Vision Skills Progression Tracker 1.0.0-beta API) - + diff --git a/target/apidocs/com/studentgui/apppages/class-use/ScreenReader.html b/target/site/apidocs/com/studentgui/apppages/class-use/ScreenReader.html similarity index 96% rename from target/apidocs/com/studentgui/apppages/class-use/ScreenReader.html rename to target/site/apidocs/com/studentgui/apppages/class-use/ScreenReader.html index 01e3352..746302d 100644 --- a/target/apidocs/com/studentgui/apppages/class-use/ScreenReader.html +++ b/target/site/apidocs/com/studentgui/apppages/class-use/ScreenReader.html @@ -1,11 +1,11 @@ - + Uses of Class com.studentgui.apppages.ScreenReader (Vision Skills Progression Tracker 1.0.0-beta API) - + diff --git a/target/apidocs/com/studentgui/apppages/class-use/SessionNotes.html b/target/site/apidocs/com/studentgui/apppages/class-use/SessionNotes.html similarity index 96% rename from target/apidocs/com/studentgui/apppages/class-use/SessionNotes.html rename to target/site/apidocs/com/studentgui/apppages/class-use/SessionNotes.html index 5c009cc..0f272b7 100644 --- a/target/apidocs/com/studentgui/apppages/class-use/SessionNotes.html +++ b/target/site/apidocs/com/studentgui/apppages/class-use/SessionNotes.html @@ -1,11 +1,11 @@ - + Uses of Class com.studentgui.apppages.SessionNotes (Vision Skills Progression Tracker 1.0.0-beta API) - + diff --git a/target/apidocs/com/studentgui/apppages/package-summary.html b/target/site/apidocs/com/studentgui/apppages/package-summary.html similarity index 84% rename from target/apidocs/com/studentgui/apppages/package-summary.html rename to target/site/apidocs/com/studentgui/apppages/package-summary.html index e89c79f..f459987 100644 --- a/target/apidocs/com/studentgui/apppages/package-summary.html +++ b/target/site/apidocs/com/studentgui/apppages/package-summary.html @@ -1,11 +1,11 @@ - + com.studentgui.apppages (Vision Skills Progression Tracker 1.0.0-beta API) - + @@ -81,31 +81,31 @@

    Package com.studentgui
    Description
    -
    Abacus skills progression UI page.
    +
    Abacus computational skills assessment page.
    -
    Braille skills progression UI page.
    +
    Braille skills progression assessment page.
    -
    Braille note-taking skills progression page.
    +
    HumanWare BrailleNote Touch Plus (BNT+) proficiency assessment page.
    -
    BrailleSense skills progression UI page.
    +
    HIMS BrailleSense productivity device proficiency assessment page.
    -
    Contact log page for storing freeform contact notes for a student.
    +
    Structured parent/guardian contact log with validation and freeform notes.
    -
    Cortical Visual Impairment (CVI) progression page.
    +
    Cortical Visual Impairment (CVI) assessment page.
    -
    Digital literacy skills progression page UI.
    +
    Digital literacy and computer skills assessment page.
    @@ -113,32 +113,31 @@

    Package com.studentgui

    -
    Instructional materials viewer panel.
    +
    Instructional materials and resources reference page.
    -
    iOS / iPadOS skills progression page.
    +
    iOS and iPadOS assistive technology proficiency assessment page.
    -
    Lightweight line chart component used across pages to display recent - assessment sessions.
    +
    Reusable JFreeChart-based line chart component for visualizing student assessment progress.
    -
    Keyboarding skills page.
    +
    Touch-typing and keyboarding skills assessment page.
    -
    Observations page for recording freeform observational notes.
    +
    Observational notes page for documenting unstructured student behaviors and progress.
    -
    ScreenReader skills progression page.
    +
    Screen reader proficiency assessment page for desktop/laptop environments.
    -
    Session notes editor page.
    +
    Freeform session notes editor for general observations and reflections.

    diff --git a/target/apidocs/com/studentgui/apppages/package-tree.html b/target/site/apidocs/com/studentgui/apppages/package-tree.html similarity index 98% rename from target/apidocs/com/studentgui/apppages/package-tree.html rename to target/site/apidocs/com/studentgui/apppages/package-tree.html index 24a89ac..ea1aaf8 100644 --- a/target/apidocs/com/studentgui/apppages/package-tree.html +++ b/target/site/apidocs/com/studentgui/apppages/package-tree.html @@ -1,11 +1,11 @@ - + com.studentgui.apppages Class Hierarchy (Vision Skills Progression Tracker 1.0.0-beta API) - + diff --git a/target/apidocs/com/studentgui/apppages/package-use.html b/target/site/apidocs/com/studentgui/apppages/package-use.html similarity index 94% rename from target/apidocs/com/studentgui/apppages/package-use.html rename to target/site/apidocs/com/studentgui/apppages/package-use.html index ab46d43..be2ff56 100644 --- a/target/apidocs/com/studentgui/apppages/package-use.html +++ b/target/site/apidocs/com/studentgui/apppages/package-use.html @@ -1,11 +1,11 @@ - + Uses of Package com.studentgui.apppages (Vision Skills Progression Tracker 1.0.0-beta API) - + @@ -69,8 +69,7 @@

    Uses of Packag
    Description
    -
    Lightweight line chart component used across pages to display recent - assessment sessions.
    +
    Reusable JFreeChart-based line chart component for visualizing student assessment progress.

  • diff --git a/target/apidocs/com/studentgui/apptheming/Theme.html b/target/site/apidocs/com/studentgui/apptheming/Theme.html similarity index 72% rename from target/apidocs/com/studentgui/apptheming/Theme.html rename to target/site/apidocs/com/studentgui/apptheming/Theme.html index 1a766b5..5b9d525 100644 --- a/target/apidocs/com/studentgui/apptheming/Theme.html +++ b/target/site/apidocs/com/studentgui/apptheming/Theme.html @@ -1,11 +1,11 @@ - + Theme (Vision Skills Progression Tracker 1.0.0-beta API) - + @@ -92,10 +92,63 @@

    Class Theme


  • -
    public class Theme +
    public class Theme extends Object
    -
    Small theming and menu helper. Constructs a simple Navigate menu used by - the main application window.
    +
    Application theming and menu bar construction utilities. + +

    Provides centralized menu bar factory for the main application window with + keyboard shortcuts, mnemonics, and accessibility support. The menu structure + organizes assessment pages into logical categories:

    + +
      +
    • Navigate Menu: Primary navigation menu containing: +
        +
      • Home: Returns to homepage (Ctrl+Alt+H)
      • +
      • Tactile Submenu: Braille and Abacus skills pages (alphabetical)
      • +
      • Technology Submenu: Device-specific pages (BrailleNote, BrailleSense, iOS, ScreenReader, etc.)
      • +
      • Communication Submenu: Contact Log and Session Notes
      • +
      • Other Skills Submenu: CVI, Digital Literacy, Keyboarding, Observations, Instructional Materials
      • +
      +
    • +
    + +

    Accessibility Features:

    +
      +
    • All menu items include accessible names and descriptions
    • +
    • Keyboard shortcuts use Ctrl+Alt+Letter combinations to avoid conflicts
    • +
    • Mnemonics provided for primary menu items (Alt+H for Home, etc.)
    • +
    • Color-coded icons generated programmatically via makeIcon(Color, int)
    • +
    + +

    Icon Generation: Menu items display small colored square icons for + visual differentiation. Icons are generated at runtime as 12×12px BufferedImage + instances with anti-aliased rendering for smooth appearance across themes.

    + +

    Menu Structure Rationale:

    +
      +
    • Tactile skills (Braille, Abacus) grouped separately from technology devices
    • +
    • Technology submenu organized by device type (notetakers, mobile OS, desktop screen readers)
    • +
    • Communication tools (Contact Log, Session Notes) kept together for workflow consistency
    • +
    • Remaining assessment pages grouped under "Other Skills" for flexibility
    • +
    + +

    Navigation Integration: All menu items invoke the main navigation logic in Main + to switch the main content panel. Page identifiers are lowercase strings matching page class names + (e.g., "braille", "abacus", "braillenote").

    + +

    Theme Management: Currently limited to menu bar construction. Future expansion + may include FlatLaf theme switching, custom color schemes, or icon set selection.

    +
    +
    See Also:
    +
    + +
    +
      @@ -135,7 +188,7 @@

      Method Details

    • createMenuBar

      -
      public static JMenuBar createMenuBar()
      +
      public static JMenuBar createMenuBar()
      Build and return the application menu bar used in the main frame.
      Returns:
      diff --git a/target/apidocs/com/studentgui/apptheming/class-use/Theme.html b/target/site/apidocs/com/studentgui/apptheming/class-use/Theme.html similarity index 96% rename from target/apidocs/com/studentgui/apptheming/class-use/Theme.html rename to target/site/apidocs/com/studentgui/apptheming/class-use/Theme.html index 7f58e3b..4c49113 100644 --- a/target/apidocs/com/studentgui/apptheming/class-use/Theme.html +++ b/target/site/apidocs/com/studentgui/apptheming/class-use/Theme.html @@ -1,11 +1,11 @@ - + Uses of Class com.studentgui.apptheming.Theme (Vision Skills Progression Tracker 1.0.0-beta API) - + diff --git a/target/apidocs/com/studentgui/apptheming/package-summary.html b/target/site/apidocs/com/studentgui/apptheming/package-summary.html similarity index 95% rename from target/apidocs/com/studentgui/apptheming/package-summary.html rename to target/site/apidocs/com/studentgui/apptheming/package-summary.html index d2e1b7d..4a36fe4 100644 --- a/target/apidocs/com/studentgui/apptheming/package-summary.html +++ b/target/site/apidocs/com/studentgui/apptheming/package-summary.html @@ -1,11 +1,11 @@ - + com.studentgui.apptheming (Vision Skills Progression Tracker 1.0.0-beta API) - + @@ -81,7 +81,7 @@

      Package com.studentg
      Description
      -
      Small theming and menu helper.
      +
      Application theming and menu bar construction utilities.

  • diff --git a/target/apidocs/com/studentgui/apptheming/package-tree.html b/target/site/apidocs/com/studentgui/apptheming/package-tree.html similarity index 96% rename from target/apidocs/com/studentgui/apptheming/package-tree.html rename to target/site/apidocs/com/studentgui/apptheming/package-tree.html index 5b04ff5..e3f976b 100644 --- a/target/apidocs/com/studentgui/apptheming/package-tree.html +++ b/target/site/apidocs/com/studentgui/apptheming/package-tree.html @@ -1,11 +1,11 @@ - + com.studentgui.apptheming Class Hierarchy (Vision Skills Progression Tracker 1.0.0-beta API) - + diff --git a/target/apidocs/com/studentgui/apptheming/package-use.html b/target/site/apidocs/com/studentgui/apptheming/package-use.html similarity index 96% rename from target/apidocs/com/studentgui/apptheming/package-use.html rename to target/site/apidocs/com/studentgui/apptheming/package-use.html index 25f6f2e..2106dfd 100644 --- a/target/apidocs/com/studentgui/apptheming/package-use.html +++ b/target/site/apidocs/com/studentgui/apptheming/package-use.html @@ -1,11 +1,11 @@ - + Uses of Package com.studentgui.apptheming (Vision Skills Progression Tracker 1.0.0-beta API) - + diff --git a/target/apidocs/com/studentgui/bootstrap/Bootstrap.html b/target/site/apidocs/com/studentgui/bootstrap/Bootstrap.html similarity index 94% rename from target/apidocs/com/studentgui/bootstrap/Bootstrap.html rename to target/site/apidocs/com/studentgui/bootstrap/Bootstrap.html index 7307879..f857f87 100644 --- a/target/apidocs/com/studentgui/bootstrap/Bootstrap.html +++ b/target/site/apidocs/com/studentgui/bootstrap/Bootstrap.html @@ -1,11 +1,11 @@ - + Bootstrap (Vision Skills Progression Tracker 1.0.0-beta API) - + @@ -92,7 +92,7 @@

    Class Bootstrap


    -
    public final class Bootstrap +
    public final class Bootstrap extends Object
    Lightweight bootstrapper that sets early system properties required by the logging subsystem (APP_HOME and LOG_TS) before delegating to the @@ -135,7 +135,7 @@

    Method Details

  • main

    -
    public static void main(String[] args)
    +
    public static void main(String[] args)
  • diff --git a/target/apidocs/com/studentgui/bootstrap/class-use/Bootstrap.html b/target/site/apidocs/com/studentgui/bootstrap/class-use/Bootstrap.html similarity index 96% rename from target/apidocs/com/studentgui/bootstrap/class-use/Bootstrap.html rename to target/site/apidocs/com/studentgui/bootstrap/class-use/Bootstrap.html index e0d875b..30fc146 100644 --- a/target/apidocs/com/studentgui/bootstrap/class-use/Bootstrap.html +++ b/target/site/apidocs/com/studentgui/bootstrap/class-use/Bootstrap.html @@ -1,11 +1,11 @@ - + Uses of Class com.studentgui.bootstrap.Bootstrap (Vision Skills Progression Tracker 1.0.0-beta API) - + diff --git a/target/apidocs/com/studentgui/bootstrap/package-summary.html b/target/site/apidocs/com/studentgui/bootstrap/package-summary.html similarity index 97% rename from target/apidocs/com/studentgui/bootstrap/package-summary.html rename to target/site/apidocs/com/studentgui/bootstrap/package-summary.html index e16be5e..cc55ca3 100644 --- a/target/apidocs/com/studentgui/bootstrap/package-summary.html +++ b/target/site/apidocs/com/studentgui/bootstrap/package-summary.html @@ -1,11 +1,11 @@ - + com.studentgui.bootstrap (Vision Skills Progression Tracker 1.0.0-beta API) - + diff --git a/target/apidocs/com/studentgui/bootstrap/package-tree.html b/target/site/apidocs/com/studentgui/bootstrap/package-tree.html similarity index 96% rename from target/apidocs/com/studentgui/bootstrap/package-tree.html rename to target/site/apidocs/com/studentgui/bootstrap/package-tree.html index 3582871..c20cbe4 100644 --- a/target/apidocs/com/studentgui/bootstrap/package-tree.html +++ b/target/site/apidocs/com/studentgui/bootstrap/package-tree.html @@ -1,11 +1,11 @@ - + com.studentgui.bootstrap Class Hierarchy (Vision Skills Progression Tracker 1.0.0-beta API) - + diff --git a/target/apidocs/com/studentgui/bootstrap/package-use.html b/target/site/apidocs/com/studentgui/bootstrap/package-use.html similarity index 96% rename from target/apidocs/com/studentgui/bootstrap/package-use.html rename to target/site/apidocs/com/studentgui/bootstrap/package-use.html index 6d749e5..6982ae9 100644 --- a/target/apidocs/com/studentgui/bootstrap/package-use.html +++ b/target/site/apidocs/com/studentgui/bootstrap/package-use.html @@ -1,11 +1,11 @@ - + Uses of Package com.studentgui.bootstrap (Vision Skills Progression Tracker 1.0.0-beta API) - + diff --git a/target/apidocs/com/studentgui/test/BrailleSmokeTest.html b/target/site/apidocs/com/studentgui/test/BrailleSmokeTest.html similarity index 96% rename from target/apidocs/com/studentgui/test/BrailleSmokeTest.html rename to target/site/apidocs/com/studentgui/test/BrailleSmokeTest.html index 6469875..680b9e9 100644 --- a/target/apidocs/com/studentgui/test/BrailleSmokeTest.html +++ b/target/site/apidocs/com/studentgui/test/BrailleSmokeTest.html @@ -1,11 +1,11 @@ - + BrailleSmokeTest (Vision Skills Progression Tracker 1.0.0-beta API) - + @@ -93,7 +93,7 @@

    Class BrailleSmokeTest


    @Deprecated -public final class BrailleSmokeTest +public final class BrailleSmokeTest extends Object
    Deprecated.
    Use src/test/java/com/studentgui/test/BrailleSmokeTest.java diff --git a/target/apidocs/com/studentgui/test/class-use/BrailleSmokeTest.html b/target/site/apidocs/com/studentgui/test/class-use/BrailleSmokeTest.html similarity index 96% rename from target/apidocs/com/studentgui/test/class-use/BrailleSmokeTest.html rename to target/site/apidocs/com/studentgui/test/class-use/BrailleSmokeTest.html index eb7242f..bb6c3f7 100644 --- a/target/apidocs/com/studentgui/test/class-use/BrailleSmokeTest.html +++ b/target/site/apidocs/com/studentgui/test/class-use/BrailleSmokeTest.html @@ -1,11 +1,11 @@ - + Uses of Class com.studentgui.test.BrailleSmokeTest (Vision Skills Progression Tracker 1.0.0-beta API) - + diff --git a/target/apidocs/com/studentgui/test/package-summary.html b/target/site/apidocs/com/studentgui/test/package-summary.html similarity index 97% rename from target/apidocs/com/studentgui/test/package-summary.html rename to target/site/apidocs/com/studentgui/test/package-summary.html index 37292be..1d0b804 100644 --- a/target/apidocs/com/studentgui/test/package-summary.html +++ b/target/site/apidocs/com/studentgui/test/package-summary.html @@ -1,11 +1,11 @@ - + com.studentgui.test (Vision Skills Progression Tracker 1.0.0-beta API) - + diff --git a/target/apidocs/com/studentgui/test/package-tree.html b/target/site/apidocs/com/studentgui/test/package-tree.html similarity index 96% rename from target/apidocs/com/studentgui/test/package-tree.html rename to target/site/apidocs/com/studentgui/test/package-tree.html index 950fd90..b841631 100644 --- a/target/apidocs/com/studentgui/test/package-tree.html +++ b/target/site/apidocs/com/studentgui/test/package-tree.html @@ -1,11 +1,11 @@ - + com.studentgui.test Class Hierarchy (Vision Skills Progression Tracker 1.0.0-beta API) - + diff --git a/target/apidocs/com/studentgui/test/package-use.html b/target/site/apidocs/com/studentgui/test/package-use.html similarity index 96% rename from target/apidocs/com/studentgui/test/package-use.html rename to target/site/apidocs/com/studentgui/test/package-use.html index ce8203d..77148b5 100644 --- a/target/apidocs/com/studentgui/test/package-use.html +++ b/target/site/apidocs/com/studentgui/test/package-use.html @@ -1,11 +1,11 @@ - + Uses of Package com.studentgui.test (Vision Skills Progression Tracker 1.0.0-beta API) - + diff --git a/target/apidocs/com/studentgui/tools/GroupedSmoke.html b/target/site/apidocs/com/studentgui/tools/GroupedSmoke.html similarity index 77% rename from target/apidocs/com/studentgui/tools/GroupedSmoke.html rename to target/site/apidocs/com/studentgui/tools/GroupedSmoke.html index a6a5b25..207f1a0 100644 --- a/target/apidocs/com/studentgui/tools/GroupedSmoke.html +++ b/target/site/apidocs/com/studentgui/tools/GroupedSmoke.html @@ -1,11 +1,11 @@ - + GroupedSmoke (Vision Skills Progression Tracker 1.0.0-beta API) - + @@ -92,11 +92,57 @@

    Class GroupedSmoke


    -
    public class GroupedSmoke +
    public class GroupedSmoke extends Object
    -
    Small command-line helper that renders a sample grouped chart and - writes an output PNG to the app data folder. Intended for smoke - testing chart rendering during development and CI.
    +
    Automated smoke test for grouped chart rendering and multi-panel PNG export. + +

    Verifies that JLineGraph correctly renders multiple stacked phase-grouped + charts (as used by assessment pages like Braille, Abacus, etc.). Generates synthetic + data with explicit phase prefixes (P1, P2, P3) and exports to PNG.

    + +

    Purpose:

    + + +

    Usage:

    +
    
    + java -cp StudentDataGUI.jar com.studentgui.tools.GroupedSmoke
    + 
    + +

    Expected Output:

    +
    + Grouped smoke wrote chart to: /path/to/app_home/StudentDataFiles/Grouped_Smoke/plots/GroupedSmoke-2024-01-15.png
    + Exists: true
    + 
    + +

    Test Data Structure:

    +
      +
    • Part codes: 9 codes with prefixes: P1 (3 items), P2 (2 items), P3 (4 items)
    • +
    • Sessions: 3 synthetic sessions with deterministic scores (i + s) % 5
    • +
    • Expected output: 3 stacked chart panels (one per phase group) in a single 800×600px PNG
    • +
    + +

    Output Location: app_home/StudentDataFiles/Grouped_Smoke/plots/GroupedSmoke-<ISO_DATE>.png

    + +

    Validation: Inspect the generated PNG to verify:

    +
      +
    1. Three distinct chart panels labeled "P1 - 3 items", "P2 - 2 items", "P3 - 4 items"
    2. +
    3. Each panel shows 3 line series (2 gray historical, 1 black latest)
    4. +
    5. Colored background bands visible in all panels
    6. +
    +
    +
    See Also:
    +
    + +
    +
      @@ -151,7 +197,7 @@

      Constructor Details

    • GroupedSmoke

      -
      public GroupedSmoke()
      +
      public GroupedSmoke()
      Public no-arg constructor to document the utility nature of this class. Kept for completeness; all work is performed from main(String[]).
      @@ -167,7 +213,7 @@

      Method Details

    • main

      -
      public static void main(String[] args) +
      public static void main(String[] args) throws Exception
      Entry point for the grouped smoke utility.
      diff --git a/target/apidocs/com/studentgui/tools/ProgrammaticPageSaveTest.html b/target/site/apidocs/com/studentgui/tools/ProgrammaticPageSaveTest.html similarity index 72% rename from target/apidocs/com/studentgui/tools/ProgrammaticPageSaveTest.html rename to target/site/apidocs/com/studentgui/tools/ProgrammaticPageSaveTest.html index 3f71a33..acf2cfe 100644 --- a/target/apidocs/com/studentgui/tools/ProgrammaticPageSaveTest.html +++ b/target/site/apidocs/com/studentgui/tools/ProgrammaticPageSaveTest.html @@ -1,11 +1,11 @@ - + ProgrammaticPageSaveTest (Vision Skills Progression Tracker 1.0.0-beta API) - + @@ -92,15 +92,62 @@

      Class ProgrammaticPageS


      -
      public class ProgrammaticPageSaveTest +
      public class ProgrammaticPageSaveTest extends Object
      -
      Programmatically create a Braille page, populate PhaseScoreField values, - and trigger the submit action to verify DB insert and PNG export. -

      - This small test runs without user interaction and is useful during - automated smoke tests or developer verification of page submission - behaviour. Outputs are written under the application's app_home. -

      +
      Automated integration test for programmatic page manipulation and database submission. + +

      Simulates user interaction with the Braille assessment page by:

      +
        +
      1. Programmatically instantiating a Braille page with synthetic student/date
      2. +
      3. Using reflection to access and populate internal PhaseScoreField components
      4. +
      5. Locating the "Submit Braille Data" button via accessible name
      6. +
      7. Programmatically triggering the submit action via AbstractButton.doClick()
      8. +
      + +

      Purpose:

      +
        +
      • Validates end-to-end page submission workflow without GUI interaction
      • +
      • Tests database insert, JSON export, and PNG chart generation in automated context
      • +
      • Verifies reflection-based access to page internals for testing purposes
      • +
      • Provides reference for programmatic testing of other assessment pages
      • +
      + +

      Usage:

      +
      
      + java -cp StudentDataGUI.jar com.studentgui.tools.ProgrammaticPageSaveTest
      + 
      + +

      Expected Side Effects:

      +
        +
      • New Braille progress session inserted into database for student "Smoke Test"
      • +
      • JSON export written to StudentDataFiles/Smoke_Test/Sessions/Braille/
      • +
      • Phase-grouped PNG plots written to StudentDataFiles/Smoke_Test/plots/
      • +
      • Markdown and HTML reports generated in StudentDataFiles/Smoke_Test/reports/
      • +
      + +

      Reflection Usage: Accesses private skillFields array in Braille + to set all 64 Braille skills to a score of 3. This demonstrates how to programmatically + manipulate page state for testing when public setters are not available.

      + +

      Validation: After execution, inspect:

      +
        +
      • Database: sqlite3 app_home/StudentDatabase/students.db "SELECT * FROM ProgressSession ORDER BY id DESC LIMIT 1;"
      • +
      • JSON exports: ls -lt app_home/StudentDataFiles/Smoke_Test/Sessions/Braille/
      • +
      • Generated plots: ls -lt app_home/StudentDataFiles/Smoke_Test/plots/
      • +
      + +

      Note: This test modifies the live database. Run in a test environment or + use a separate APP_HOME directory to avoid polluting production data.

      +
      +
      See Also:
      +
      + +
      +
        @@ -140,7 +187,7 @@

        Method Details

      • main

        -
        public static void main(String[] args) +
        public static void main(String[] args) throws Exception
        Program entry to run the programmatic page save test.
        diff --git a/target/apidocs/com/studentgui/tools/QueryStudentData.html b/target/site/apidocs/com/studentgui/tools/QueryStudentData.html similarity index 80% rename from target/apidocs/com/studentgui/tools/QueryStudentData.html rename to target/site/apidocs/com/studentgui/tools/QueryStudentData.html index a042fd7..7e9113f 100644 --- a/target/apidocs/com/studentgui/tools/QueryStudentData.html +++ b/target/site/apidocs/com/studentgui/tools/QueryStudentData.html @@ -1,11 +1,11 @@ - + QueryStudentData (Vision Skills Progression Tracker 1.0.0-beta API) - + @@ -92,11 +92,54 @@

        Class QueryStudentData


        -
        public class QueryStudentData +
        public class QueryStudentData extends Object
        -
        Development utility to inspect available students and their recent - progress session rows. Prints basic statistics to stdout and is - intended for debugging or manual data inspection.
        +
        Command-line inspection tool for viewing student database contents and schema statistics. + +

        Provides a quick diagnostic view of database state without launching the GUI. + Useful for:

        +
          +
        • Verifying student records exist in the database
        • +
        • Inspecting available progress types and their assessment part counts
        • +
        • Checking session data row sizes for debugging schema migrations
        • +
        • Quick manual data verification during development or troubleshooting
        • +
        + +

        Usage:

        +
        
        + # List all students and progress types with counts
        + java -cp StudentDataGUI.jar com.studentgui.tools.QueryStudentData
        +
        + # Inspect specific student's progress types
        + java -cp StudentDataGUI.jar com.studentgui.tools.QueryStudentData "Aaron A Aaronsson"
        + 
        + +

        Output Format:

        +
        + Inspecting student: Aaron A Aaronsson
        + ProgressType 'Braille' (id=1) parts=64 sessions=3
        +  Sample row sizes: 64 values: [2, 3, 2, 3, 4, ...]
        + ProgressType 'Abacus' (id=2) parts=22 sessions=1
        +  Sample row sizes: 22 values: [0, 1, 2, 1, 3, ...]
        + 
        + +

        Workflow:

        +
          +
        1. Lists all known students via Helpers.getStudents()
        2. +
        3. Selects first student or uses command-line argument
        4. +
        5. Queries ProgressType table for all progress types
        6. +
        7. For each progress type: counts assessment parts and fetches sample session rows
        8. +
        9. Prints progress type name, ID, part count, session count, and sample row to stdout
        10. +
        +
        +
        See Also:
        +
        + +
        +
          @@ -151,7 +194,7 @@

          Constructor Details

        • QueryStudentData

          -
          public QueryStudentData()
          +
          No-op public constructor to document this class as a small utility.
        • @@ -166,7 +209,7 @@

          Method Details

        • main

          -
          public static void main(String[] args) +
          public static void main(String[] args) throws Exception
          Command-line entry point. Prints progress types and a sample row for the specified or first-known student.
          diff --git a/target/apidocs/com/studentgui/tools/RenderStudentProgress.html b/target/site/apidocs/com/studentgui/tools/RenderStudentProgress.html similarity index 78% rename from target/apidocs/com/studentgui/tools/RenderStudentProgress.html rename to target/site/apidocs/com/studentgui/tools/RenderStudentProgress.html index cf8488a..1cfe30e 100644 --- a/target/apidocs/com/studentgui/tools/RenderStudentProgress.html +++ b/target/site/apidocs/com/studentgui/tools/RenderStudentProgress.html @@ -1,11 +1,11 @@ - + RenderStudentProgress (Vision Skills Progression Tracker 1.0.0-beta API) - + @@ -92,11 +92,45 @@

          Class RenderStudentProgres


          -
          public class RenderStudentProgress +
          public class RenderStudentProgress extends Object
          -
          Command-line tool to render a particular student's progress chart - for a named progress type. Produces a PNG in the student's plots - directory. Useful for offline rendering and debugging chart output.
          +
          Command-line utility for offline student progress chart rendering and export. + +

          This standalone tool generates PNG charts for a specific student and progress type + without launching the full GUI application. Useful for:

          +
            +
          • Batch chart generation for multiple students/progress types
          • +
          • Debugging chart rendering issues outside the GUI context
          • +
          • Automated report generation in CI/CD pipelines
          • +
          • Creating historical chart snapshots for archival purposes
          • +
          + +

          Usage:

          +
          
          + java -cp StudentDataGUI.jar com.studentgui.tools.RenderStudentProgress "Aaron A Aaronsson" "Braille"
          + 
          + +

          Workflow:

          +
            +
          1. Ensures app folder hierarchy exists via Helpers.createFolderHierarchy()
          2. +
          3. Queries database for canonical assessment part codes for the specified progress type
          4. +
          5. Fetches up to 5 most recent assessment sessions via Database.fetchLatestAssessmentResults(java.lang.String, java.lang.String, int)
          6. +
          7. Renders grouped chart using JLineGraph.updateWithGroupedData(java.util.List<java.util.List<java.lang.Integer>>, java.lang.String[])
          8. +
          9. Exports PNG to StudentDataFiles/<student>/plots/<ProgressType>-render-<date>.png
          10. +
          + +

          Output: PNG file written to student's plots directory with filename format: + <ProgressType>-render-<ISO_DATE>.png

          +
          +
          See Also:
          +
          + +
          +
            @@ -151,7 +185,7 @@

            Constructor Details

          • RenderStudentProgress

            -
            public RenderStudentProgress()
            +
            Explicit no-arg constructor with documentation to avoid default-constructor javadoc warnings.
          • @@ -166,7 +200,7 @@

            Method Details

          • main

            -
            public static void main(String[] args) +
            public static void main(String[] args) throws Exception
            Render and write a progress chart for the provided student and progress type.
            diff --git a/target/apidocs/com/studentgui/tools/SmokeTest.html b/target/site/apidocs/com/studentgui/tools/SmokeTest.html similarity index 78% rename from target/apidocs/com/studentgui/tools/SmokeTest.html rename to target/site/apidocs/com/studentgui/tools/SmokeTest.html index 3ad9a80..4be04d0 100644 --- a/target/apidocs/com/studentgui/tools/SmokeTest.html +++ b/target/site/apidocs/com/studentgui/tools/SmokeTest.html @@ -1,11 +1,11 @@ - + SmokeTest (Vision Skills Progression Tracker 1.0.0-beta API) - + @@ -92,13 +92,49 @@

            Class SmokeTest


            -
            public class SmokeTest +
            public class SmokeTest extends Object
            -
            Minimal smoke test to exercise the chart rendering and file export. -

            - Generates deterministic sample data, renders it via JLineGraph - and writes a PNG under the app_home plots directory. -

            +
            Minimal automated smoke test for chart rendering and PNG export functionality. + +

            Generates deterministic synthetic assessment data, renders it via JLineGraph, + and writes a PNG to the app data folder. Used to verify:

            +
              +
            • JFreeChart rendering pipeline functions correctly
            • +
            • PNG export via JLineGraph.saveChart(java.nio.file.Path, int, int) produces valid image files
            • +
            • File I/O permissions and folder creation work as expected
            • +
            • Chart layout and visual appearance match expectations (manual review)
            • +
            + +

            Usage:

            +
            
            + java -cp StudentDataGUI.jar com.studentgui.tools.SmokeTest
            + 
            + +

            Expected Output:

            +
            + Smoke test wrote chart to: /path/to/app_home/StudentDataFiles/Smoke_Test/plots/SmokeTest-2024-01-15.png
            + Exists: true
            + 
            + +

            Test Data: Generates 3 synthetic sessions with 28 skills each, using + the formula (skillIndex + sessionIndex) % 5 to produce deterministic + values in the 0–4 range.

            + +

            Output Location: app_home/StudentDataFiles/Smoke_Test/plots/SmokeTest-<ISO_DATE>.png

            + +

            Validation: Success is indicated by "Exists: true" output and a valid + 800×400px PNG file at the reported path. Visual inspection of the chart should show + 3 line series (2 gray, 1 black) with colored background bands.

            +
            +
            See Also:
            +
            + +
            +
              @@ -138,7 +174,7 @@

              Method Details

            • main

              -
              public static void main(String[] args) +
              public static void main(String[] args) throws Exception
              Entry point for the smoke test.
              diff --git a/target/apidocs/com/studentgui/tools/class-use/GroupedSmoke.html b/target/site/apidocs/com/studentgui/tools/class-use/GroupedSmoke.html similarity index 96% rename from target/apidocs/com/studentgui/tools/class-use/GroupedSmoke.html rename to target/site/apidocs/com/studentgui/tools/class-use/GroupedSmoke.html index 8f1085c..884051d 100644 --- a/target/apidocs/com/studentgui/tools/class-use/GroupedSmoke.html +++ b/target/site/apidocs/com/studentgui/tools/class-use/GroupedSmoke.html @@ -1,11 +1,11 @@ - + Uses of Class com.studentgui.tools.GroupedSmoke (Vision Skills Progression Tracker 1.0.0-beta API) - + diff --git a/target/apidocs/com/studentgui/tools/class-use/ProgrammaticPageSaveTest.html b/target/site/apidocs/com/studentgui/tools/class-use/ProgrammaticPageSaveTest.html similarity index 96% rename from target/apidocs/com/studentgui/tools/class-use/ProgrammaticPageSaveTest.html rename to target/site/apidocs/com/studentgui/tools/class-use/ProgrammaticPageSaveTest.html index 748360c..fdd5a56 100644 --- a/target/apidocs/com/studentgui/tools/class-use/ProgrammaticPageSaveTest.html +++ b/target/site/apidocs/com/studentgui/tools/class-use/ProgrammaticPageSaveTest.html @@ -1,11 +1,11 @@ - + Uses of Class com.studentgui.tools.ProgrammaticPageSaveTest (Vision Skills Progression Tracker 1.0.0-beta API) - + diff --git a/target/apidocs/com/studentgui/tools/class-use/QueryStudentData.html b/target/site/apidocs/com/studentgui/tools/class-use/QueryStudentData.html similarity index 96% rename from target/apidocs/com/studentgui/tools/class-use/QueryStudentData.html rename to target/site/apidocs/com/studentgui/tools/class-use/QueryStudentData.html index deccd2a..609558e 100644 --- a/target/apidocs/com/studentgui/tools/class-use/QueryStudentData.html +++ b/target/site/apidocs/com/studentgui/tools/class-use/QueryStudentData.html @@ -1,11 +1,11 @@ - + Uses of Class com.studentgui.tools.QueryStudentData (Vision Skills Progression Tracker 1.0.0-beta API) - + diff --git a/target/apidocs/com/studentgui/tools/class-use/RenderStudentProgress.html b/target/site/apidocs/com/studentgui/tools/class-use/RenderStudentProgress.html similarity index 96% rename from target/apidocs/com/studentgui/tools/class-use/RenderStudentProgress.html rename to target/site/apidocs/com/studentgui/tools/class-use/RenderStudentProgress.html index 5ecaacd..14941f8 100644 --- a/target/apidocs/com/studentgui/tools/class-use/RenderStudentProgress.html +++ b/target/site/apidocs/com/studentgui/tools/class-use/RenderStudentProgress.html @@ -1,11 +1,11 @@ - + Uses of Class com.studentgui.tools.RenderStudentProgress (Vision Skills Progression Tracker 1.0.0-beta API) - + diff --git a/target/apidocs/com/studentgui/tools/class-use/SmokeTest.html b/target/site/apidocs/com/studentgui/tools/class-use/SmokeTest.html similarity index 96% rename from target/apidocs/com/studentgui/tools/class-use/SmokeTest.html rename to target/site/apidocs/com/studentgui/tools/class-use/SmokeTest.html index 48d7a4c..2efea86 100644 --- a/target/apidocs/com/studentgui/tools/class-use/SmokeTest.html +++ b/target/site/apidocs/com/studentgui/tools/class-use/SmokeTest.html @@ -1,11 +1,11 @@ - + Uses of Class com.studentgui.tools.SmokeTest (Vision Skills Progression Tracker 1.0.0-beta API) - + diff --git a/target/apidocs/com/studentgui/tools/package-summary.html b/target/site/apidocs/com/studentgui/tools/package-summary.html similarity index 86% rename from target/apidocs/com/studentgui/tools/package-summary.html rename to target/site/apidocs/com/studentgui/tools/package-summary.html index 185a0dd..551a312 100644 --- a/target/apidocs/com/studentgui/tools/package-summary.html +++ b/target/site/apidocs/com/studentgui/tools/package-summary.html @@ -1,11 +1,11 @@ - + com.studentgui.tools (Vision Skills Progression Tracker 1.0.0-beta API) - + @@ -81,27 +81,23 @@

              Package com.studentgui.to
              Description
              -
              Small command-line helper that renders a sample grouped chart and - writes an output PNG to the app data folder.
              +
              Automated smoke test for grouped chart rendering and multi-panel PNG export.
              -
              Programmatically create a Braille page, populate PhaseScoreField values, - and trigger the submit action to verify DB insert and PNG export.
              +
              Automated integration test for programmatic page manipulation and database submission.
              -
              Development utility to inspect available students and their recent - progress session rows.
              +
              Command-line inspection tool for viewing student database contents and schema statistics.
              -
              Command-line tool to render a particular student's progress chart - for a named progress type.
              +
              Command-line utility for offline student progress chart rendering and export.
              -
              Minimal smoke test to exercise the chart rendering and file export.
              +
              Minimal automated smoke test for chart rendering and PNG export functionality.

    diff --git a/target/apidocs/com/studentgui/tools/package-tree.html b/target/site/apidocs/com/studentgui/tools/package-tree.html similarity index 97% rename from target/apidocs/com/studentgui/tools/package-tree.html rename to target/site/apidocs/com/studentgui/tools/package-tree.html index 6f205db..683110a 100644 --- a/target/apidocs/com/studentgui/tools/package-tree.html +++ b/target/site/apidocs/com/studentgui/tools/package-tree.html @@ -1,11 +1,11 @@ - + com.studentgui.tools Class Hierarchy (Vision Skills Progression Tracker 1.0.0-beta API) - + diff --git a/target/apidocs/com/studentgui/tools/package-use.html b/target/site/apidocs/com/studentgui/tools/package-use.html similarity index 96% rename from target/apidocs/com/studentgui/tools/package-use.html rename to target/site/apidocs/com/studentgui/tools/package-use.html index a6f0edd..ad2b48c 100644 --- a/target/apidocs/com/studentgui/tools/package-use.html +++ b/target/site/apidocs/com/studentgui/tools/package-use.html @@ -1,11 +1,11 @@ - + Uses of Package com.studentgui.tools (Vision Skills Progression Tracker 1.0.0-beta API) - + diff --git a/target/apidocs/com/studentgui/uicomp/PhaseScoreField.html b/target/site/apidocs/com/studentgui/uicomp/PhaseScoreField.html similarity index 97% rename from target/apidocs/com/studentgui/uicomp/PhaseScoreField.html rename to target/site/apidocs/com/studentgui/uicomp/PhaseScoreField.html index 6f387a9..be5012d 100644 --- a/target/apidocs/com/studentgui/uicomp/PhaseScoreField.html +++ b/target/site/apidocs/com/studentgui/uicomp/PhaseScoreField.html @@ -1,11 +1,11 @@ - + PhaseScoreField (Vision Skills Progression Tracker 1.0.0-beta API) - + @@ -104,7 +104,7 @@

    Class PhaseScoreField

    ImageObserver, MenuContainer, Serializable, Accessible

  • -
    public class PhaseScoreField +
    public class PhaseScoreField extends JPanel
    Reusable component that renders a wrapped descriptive label and a compact integer input (0..4). The label is a non-editable JTextArea that wraps at @@ -269,7 +269,7 @@

    Constructor Details

  • PhaseScoreField

    -
    public PhaseScoreField(String labelText, +
    public PhaseScoreField(String labelText, int initial)
    Create a PhaseScoreField containing a wrapped label and a numeric spinner.
    @@ -290,7 +290,7 @@

    Method Details

  • setGlobalLabelWidth

    -
    public static void setGlobalLabelWidth(int px)
    +
    public static void setGlobalLabelWidth(int px)
    Set a global label width used by all PhaseScoreField instances created after calling this method. This helps align the spinner input across multiple rows so the entry fields start at a consistent position.
    @@ -303,7 +303,7 @@

    setGlobalLabelWidth

  • getGlobalLabelWidth

    -
    public static int getGlobalLabelWidth()
    +
    public static int getGlobalLabelWidth()
    Return the configured global label width in pixels used by new instances.
    Returns:
    @@ -314,7 +314,7 @@

    getGlobalLabelWidth

  • computeMaxLabelPixelWidth

    -
    public static int computeMaxLabelPixelWidth(Font font, +
    public static int computeMaxLabelPixelWidth(Font font, String[] labels)
    Compute the pixel width of the longest label string using the given font. Returns the maximum string width in pixels.
    @@ -330,7 +330,7 @@

    computeMaxLabelPixelWidth

  • setLabel

    -
    public void setLabel(String text)
    +
    public void setLabel(String text)
    Set the visible label text for this row.
    Parameters:
    @@ -341,7 +341,7 @@

    setLabel

  • getLabel

    -
    public String getLabel()
    +
    public String getLabel()
    Get the current label text for this field.
    Returns:
    @@ -352,7 +352,7 @@

    getLabel

  • getValue

    -
    public int getValue()
    +
    public int getValue()
    Get the integer value currently selected in the spinner.
    Returns:
    @@ -363,7 +363,7 @@

    getValue

  • setValue

    -
    public void setValue(int v)
    +
    public void setValue(int v)
    Set the spinner value clamped to the valid range (0..4).
    Parameters:
    @@ -374,7 +374,7 @@

    setValue

  • setName

    -
    public void setName(String name)
    +
    public void setName(String name)
    Overrides:
    setName in class Component
    @@ -384,7 +384,7 @@

    setName

  • getSpinnerX

    -
    public int getSpinnerX()
    +
    public int getSpinnerX()
    Get the X coordinate of the spinner inside this component (pixels).
    Returns:
    @@ -395,7 +395,7 @@

    getSpinnerX

  • getLabelWrapWidth

    -
    public int getLabelWrapWidth()
    +
    public int getLabelWrapWidth()
    Return the configured label wrap container's current width in pixels.
    Returns:
    @@ -406,7 +406,7 @@

    getLabelWrapWidth

  • getActualGap

    -
    public int getActualGap()
    +
    public int getActualGap()
    Actual horizontal gap in pixels between the label wrap right edge and the spinner left edge.
    Returns:
    diff --git a/target/apidocs/com/studentgui/uicomp/class-use/PhaseScoreField.html b/target/site/apidocs/com/studentgui/uicomp/class-use/PhaseScoreField.html similarity index 96% rename from target/apidocs/com/studentgui/uicomp/class-use/PhaseScoreField.html rename to target/site/apidocs/com/studentgui/uicomp/class-use/PhaseScoreField.html index 01de9d1..7b15a40 100644 --- a/target/apidocs/com/studentgui/uicomp/class-use/PhaseScoreField.html +++ b/target/site/apidocs/com/studentgui/uicomp/class-use/PhaseScoreField.html @@ -1,11 +1,11 @@ - + Uses of Class com.studentgui.uicomp.PhaseScoreField (Vision Skills Progression Tracker 1.0.0-beta API) - + diff --git a/target/apidocs/com/studentgui/uicomp/package-summary.html b/target/site/apidocs/com/studentgui/uicomp/package-summary.html similarity index 97% rename from target/apidocs/com/studentgui/uicomp/package-summary.html rename to target/site/apidocs/com/studentgui/uicomp/package-summary.html index afdc45f..cd54a30 100644 --- a/target/apidocs/com/studentgui/uicomp/package-summary.html +++ b/target/site/apidocs/com/studentgui/uicomp/package-summary.html @@ -1,11 +1,11 @@ - + com.studentgui.uicomp (Vision Skills Progression Tracker 1.0.0-beta API) - + diff --git a/target/apidocs/com/studentgui/uicomp/package-tree.html b/target/site/apidocs/com/studentgui/uicomp/package-tree.html similarity index 97% rename from target/apidocs/com/studentgui/uicomp/package-tree.html rename to target/site/apidocs/com/studentgui/uicomp/package-tree.html index d05c2e4..8491b91 100644 --- a/target/apidocs/com/studentgui/uicomp/package-tree.html +++ b/target/site/apidocs/com/studentgui/uicomp/package-tree.html @@ -1,11 +1,11 @@ - + com.studentgui.uicomp Class Hierarchy (Vision Skills Progression Tracker 1.0.0-beta API) - + diff --git a/target/apidocs/com/studentgui/uicomp/package-use.html b/target/site/apidocs/com/studentgui/uicomp/package-use.html similarity index 96% rename from target/apidocs/com/studentgui/uicomp/package-use.html rename to target/site/apidocs/com/studentgui/uicomp/package-use.html index e8f1334..bc24143 100644 --- a/target/apidocs/com/studentgui/uicomp/package-use.html +++ b/target/site/apidocs/com/studentgui/uicomp/package-use.html @@ -1,11 +1,11 @@ - + Uses of Package com.studentgui.uicomp (Vision Skills Progression Tracker 1.0.0-beta API) - + diff --git a/target/apidocs/copy.svg b/target/site/apidocs/copy.svg similarity index 100% rename from target/apidocs/copy.svg rename to target/site/apidocs/copy.svg diff --git a/target/apidocs/deprecated-list.html b/target/site/apidocs/deprecated-list.html similarity index 97% rename from target/apidocs/deprecated-list.html rename to target/site/apidocs/deprecated-list.html index f108d3d..e97e159 100644 --- a/target/apidocs/deprecated-list.html +++ b/target/site/apidocs/deprecated-list.html @@ -1,11 +1,11 @@ - + Deprecated List (Vision Skills Progression Tracker 1.0.0-beta API) - + diff --git a/target/apidocs/element-list b/target/site/apidocs/element-list similarity index 100% rename from target/apidocs/element-list rename to target/site/apidocs/element-list diff --git a/target/apidocs/help-doc.html b/target/site/apidocs/help-doc.html similarity index 98% rename from target/apidocs/help-doc.html rename to target/site/apidocs/help-doc.html index 15092d1..39b0482 100644 --- a/target/apidocs/help-doc.html +++ b/target/site/apidocs/help-doc.html @@ -1,11 +1,11 @@ - + API Help (Vision Skills Progression Tracker 1.0.0-beta API) - + diff --git a/target/apidocs/index-all.html b/target/site/apidocs/index-all.html similarity index 97% rename from target/apidocs/index-all.html rename to target/site/apidocs/index-all.html index 4d2f1b7..20843d0 100644 --- a/target/apidocs/index-all.html +++ b/target/site/apidocs/index-all.html @@ -1,11 +1,11 @@ - + Index (Vision Skills Progression Tracker 1.0.0-beta API) - + @@ -57,7 +57,7 @@

    A

    Abacus - Class in com.studentgui.apppages
    -
    Abacus skills progression UI page.
    +
    Abacus computational skills assessment page.
    Abacus(String, LocalDate, JLineGraph) - Constructor for class com.studentgui.apppages.Abacus
    @@ -106,7 +106,7 @@

    B

    Braille - Class in com.studentgui.apppages
    -
    Braille skills progression UI page.
    +
    Braille skills progression assessment page.
    Braille(String, LocalDate, JLineGraph) - Constructor for class com.studentgui.apppages.Braille
    @@ -114,7 +114,7 @@

    B

    BrailleNote - Class in com.studentgui.apppages
    -
    Braille note-taking skills progression page.
    +
    HumanWare BrailleNote Touch Plus (BNT+) proficiency assessment page.
    BrailleNote(String, LocalDate, JLineGraph) - Constructor for class com.studentgui.apppages.BrailleNote
    @@ -122,7 +122,7 @@

    B

    BrailleSense - Class in com.studentgui.apppages
    -
    BrailleSense skills progression UI page.
    +
    HIMS BrailleSense productivity device proficiency assessment page.
    BrailleSense(String, LocalDate, JLineGraph) - Constructor for class com.studentgui.apppages.BrailleSense
    @@ -184,7 +184,7 @@

    C

    ContactLog - Class in com.studentgui.apppages
    -
    Contact log page for storing freeform contact notes for a student.
    +
    Structured parent/guardian contact log with validation and freeform notes.
    ContactLog(String, LocalDate, JLineGraph) - Constructor for class com.studentgui.apppages.ContactLog
    @@ -221,7 +221,7 @@

    C

    CVI - Class in com.studentgui.apppages
    -
    Cortical Visual Impairment (CVI) progression page.
    +
    Cortical Visual Impairment (CVI) assessment page.
    CVI(String, LocalDate, JLineGraph) - Constructor for class com.studentgui.apppages.CVI
    @@ -281,7 +281,7 @@

    D

    DigitalLiteracy - Class in com.studentgui.apppages
    -
    Digital literacy skills progression page UI.
    +
    Digital literacy and computer skills assessment page.
    DigitalLiteracy(String, LocalDate, JLineGraph) - Constructor for class com.studentgui.apppages.DigitalLiteracy
    @@ -382,8 +382,7 @@

    G

    GroupedSmoke - Class in com.studentgui.tools
    -
    Small command-line helper that renders a sample grouped chart and - writes an output PNG to the app data folder.
    +
    Automated smoke test for grouped chart rendering and multi-panel PNG export.
    GroupedSmoke() - Constructor for class com.studentgui.tools.GroupedSmoke
    @@ -421,7 +420,7 @@

    I

    InstructionalMaterials - Class in com.studentgui.apppages
    -
    Instructional materials viewer panel.
    +
    Instructional materials and resources reference page.
    InstructionalMaterials() - Constructor for class com.studentgui.apppages.InstructionalMaterials
    @@ -429,7 +428,7 @@

    I

    IOS - Class in com.studentgui.apppages
    -
    iOS / iPadOS skills progression page.
    +
    iOS and iPadOS assistive technology proficiency assessment page.
    IOS(String, LocalDate, JLineGraph) - Constructor for class com.studentgui.apppages.IOS
    @@ -448,8 +447,7 @@

    J

    JLineGraph - Class in com.studentgui.apppages
    -
    Lightweight line chart component used across pages to display recent - assessment sessions.
    +
    Reusable JFreeChart-based line chart component for visualizing student assessment progress.
    JLineGraph() - Constructor for class com.studentgui.apppages.JLineGraph
    @@ -460,7 +458,7 @@

    K

    Keyboarding - Class in com.studentgui.apppages
    -
    Keyboarding skills page.
    +
    Touch-typing and keyboarding skills assessment page.
    Keyboarding(String, LocalDate, JLineGraph) - Constructor for class com.studentgui.apppages.Keyboarding
    @@ -555,7 +553,7 @@

    O

    Observations - Class in com.studentgui.apppages
    -
    Observations page for recording freeform observational notes.
    +
    Observational notes page for documenting unstructured student behaviors and progress.
    Observations(String, LocalDate) - Constructor for class com.studentgui.apppages.Observations
    @@ -596,8 +594,7 @@

    P

    ProgrammaticPageSaveTest - Class in com.studentgui.tools
    -
    Programmatically create a Braille page, populate PhaseScoreField values, - and trigger the submit action to verify DB insert and PNG export.
    +
    Automated integration test for programmatic page manipulation and database submission.
    PROJECT_ROOT - Static variable in class com.studentgui.apphelpers.Helpers
    @@ -616,8 +613,7 @@

    Q

    QueryStudentData - Class in com.studentgui.tools
    -
    Development utility to inspect available students and their recent - progress session rows.
    +
    Command-line inspection tool for viewing student database contents and schema statistics.
    QueryStudentData() - Constructor for class com.studentgui.tools.QueryStudentData
    @@ -640,8 +636,7 @@

    R

    RenderStudentProgress - Class in com.studentgui.tools
    -
    Command-line tool to render a particular student's progress chart - for a named progress type.
    +
    Command-line utility for offline student progress chart rendering and export.
    RenderStudentProgress() - Constructor for class com.studentgui.tools.RenderStudentProgress
    @@ -692,7 +687,7 @@

    S

    ScreenReader - Class in com.studentgui.apppages
    -
    ScreenReader skills progression page.
    +
    Screen reader proficiency assessment page for desktop/laptop environments.
    ScreenReader(String, LocalDate, JLineGraph) - Constructor for class com.studentgui.apppages.ScreenReader
    @@ -720,7 +715,7 @@

    S

    SessionNotes - Class in com.studentgui.apppages
    -
    Session notes editor page.
    +
    Freeform session notes editor for general observations and reflections.
    SessionNotes(String, LocalDate, JLineGraph) - Constructor for class com.studentgui.apppages.SessionNotes
    @@ -797,7 +792,7 @@

    S

    SmokeTest - Class in com.studentgui.tools
    -
    Minimal smoke test to exercise the chart rendering and file export.
    +
    Minimal automated smoke test for chart rendering and PNG export functionality.
    specific - Variable in class com.studentgui.apphelpers.dto.ContactPayload
    @@ -849,7 +844,7 @@

    T

    Theme - Class in com.studentgui.apptheming
    -
    Small theming and menu helper.
    +
    Application theming and menu bar construction utilities.
    topic - Variable in class com.studentgui.apphelpers.dto.KeyboardingPayload
    diff --git a/target/apidocs/index.html b/target/site/apidocs/index.html similarity index 97% rename from target/apidocs/index.html rename to target/site/apidocs/index.html index a2752e6..4b87085 100644 --- a/target/apidocs/index.html +++ b/target/site/apidocs/index.html @@ -1,11 +1,11 @@ - + Overview (Vision Skills Progression Tracker 1.0.0-beta API) - + diff --git a/target/site/apidocs/legal/ADDITIONAL_LICENSE_INFO b/target/site/apidocs/legal/ADDITIONAL_LICENSE_INFO new file mode 100644 index 0000000..ff700cd --- /dev/null +++ b/target/site/apidocs/legal/ADDITIONAL_LICENSE_INFO @@ -0,0 +1,37 @@ + ADDITIONAL INFORMATION ABOUT LICENSING + +Certain files distributed by Oracle America, Inc. and/or its affiliates are +subject to the following clarification and special exception to the GPLv2, +based on the GNU Project exception for its Classpath libraries, known as the +GNU Classpath Exception. + +Note that Oracle includes multiple, independent programs in this software +package. Some of those programs are provided under licenses deemed +incompatible with the GPLv2 by the Free Software Foundation and others. +For example, the package includes programs licensed under the Apache +License, Version 2.0 and may include FreeType. Such programs are licensed +to you under their original licenses. + +Oracle facilitates your further distribution of this package by adding the +Classpath Exception to the necessary parts of its GPLv2 code, which permits +you to use that code in combination with other independent modules not +licensed under the GPLv2. However, note that this would not permit you to +commingle code under an incompatible license with Oracle's GPLv2 licensed +code by, for example, cutting and pasting such code into a file also +containing Oracle's GPLv2 licensed code and then distributing the result. + +Additionally, if you were to remove the Classpath Exception from any of the +files to which it applies and distribute the result, you would likely be +required to license some or all of the other code in that distribution under +the GPLv2 as well, and since the GPLv2 is incompatible with the license terms +of some items included in the distribution by Oracle, removing the Classpath +Exception could therefore effectively compromise your ability to further +distribute the package. + +Failing to distribute notices associated with some files may also create +unexpected legal consequences. + +Proceed with caution and we recommend that you obtain the advice of a lawyer +skilled in open source matters before removing the Classpath Exception or +making modifications to this package which may subsequently be redistributed +and/or involve the use of third party software. diff --git a/target/site/apidocs/legal/ASSEMBLY_EXCEPTION b/target/site/apidocs/legal/ASSEMBLY_EXCEPTION new file mode 100644 index 0000000..4296666 --- /dev/null +++ b/target/site/apidocs/legal/ASSEMBLY_EXCEPTION @@ -0,0 +1,27 @@ + +OPENJDK ASSEMBLY EXCEPTION + +The OpenJDK source code made available by Oracle America, Inc. (Oracle) at +openjdk.org ("OpenJDK Code") is distributed under the terms of the GNU +General Public License version 2 +only ("GPL2"), with the following clarification and special exception. + + Linking this OpenJDK Code statically or dynamically with other code + is making a combined work based on this library. Thus, the terms + and conditions of GPL2 cover the whole combination. + + As a special exception, Oracle gives you permission to link this + OpenJDK Code with certain code licensed by Oracle as indicated at + https://openjdk.org/legal/exception-modules-2007-05-08.html + ("Designated Exception Modules") to produce an executable, + regardless of the license terms of the Designated Exception Modules, + and to copy and distribute the resulting executable under GPL2, + provided that the Designated Exception Modules continue to be + governed by the licenses under which they were offered by Oracle. + +As such, it allows licensees and sublicensees of Oracle's GPL2 OpenJDK Code +to build an executable that includes those portions of necessary code that +Oracle could not provide under GPL2 (or that Oracle has provided under GPL2 +with the Classpath exception). If you modify or add to the OpenJDK code, +that new GPL2 code may still be combined with Designated Exception Modules +if the new code is made subject to this exception by its copyright holder. diff --git a/target/site/apidocs/legal/LICENSE b/target/site/apidocs/legal/LICENSE new file mode 100644 index 0000000..8b400c7 --- /dev/null +++ b/target/site/apidocs/legal/LICENSE @@ -0,0 +1,347 @@ +The GNU General Public License (GPL) + +Version 2, June 1991 + +Copyright (C) 1989, 1991 Free Software Foundation, Inc. +51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA + +Everyone is permitted to copy and distribute verbatim copies of this license +document, but changing it is not allowed. + +Preamble + +The licenses for most software are designed to take away your freedom to share +and change it. By contrast, the GNU General Public License is intended to +guarantee your freedom to share and change free software--to make sure the +software is free for all its users. This General Public License applies to +most of the Free Software Foundation's software and to any other program whose +authors commit to using it. (Some other Free Software Foundation software is +covered by the GNU Library General Public License instead.) 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 this service 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 make restrictions that forbid anyone to deny +you these rights or to ask you to surrender the rights. These restrictions +translate to certain responsibilities for you if you distribute copies of the +software, or if you modify it. + +For example, if you distribute copies of such a program, whether gratis or for +a fee, you must give the recipients all the rights that you have. 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. + +We protect your rights with two steps: (1) copyright the software, and (2) +offer you this license which gives you legal permission to copy, distribute +and/or modify the software. + +Also, for each author's protection and ours, we want to make certain that +everyone understands that there is no warranty for this free software. If the +software is modified by someone else and passed on, we want its recipients to +know that what they have is not the original, so that any problems introduced +by others will not reflect on the original authors' reputations. + +Finally, any free program is threatened constantly by software patents. We +wish to avoid the danger that redistributors of a free program will +individually obtain patent licenses, in effect making the program proprietary. +To prevent this, we have made it clear that any patent must be licensed for +everyone's free use or not licensed at all. + +The precise terms and conditions for copying, distribution and modification +follow. + +TERMS AND CONDITIONS FOR COPYING, DISTRIBUTION AND MODIFICATION + +0. This License applies to any program or other work which contains a notice +placed by the copyright holder saying it may be distributed under the terms of +this General Public License. The "Program", below, refers to any such program +or work, and a "work based on the Program" means either the Program or any +derivative work under copyright law: that is to say, a work containing the +Program or a portion of it, either verbatim or with modifications and/or +translated into another language. (Hereinafter, translation is included +without limitation in the term "modification".) Each licensee is addressed as +"you". + +Activities other than copying, distribution and modification are not covered by +this License; they are outside its scope. The act of running the Program is +not restricted, and the output from the Program is covered only if its contents +constitute a work based on the Program (independent of having been made by +running the Program). Whether that is true depends on what the Program does. + +1. You may copy and distribute 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 and +disclaimer of warranty; keep intact all the notices that refer to this License +and to the absence of any warranty; and give any other recipients of the +Program a copy of this License along with the Program. + +You may charge a fee for the physical act of transferring a copy, and you may +at your option offer warranty protection in exchange for a fee. + +2. You may modify your copy or copies of the Program or any portion of it, thus +forming a work based on the Program, and copy and distribute such modifications +or work under the terms of Section 1 above, provided that you also meet all of +these conditions: + + a) You must cause the modified files to carry prominent notices stating + that you changed the files and the date of any change. + + b) You must cause any work that you distribute or publish, that in whole or + in part contains or is derived from the Program or any part thereof, to be + licensed as a whole at no charge to all third parties under the terms of + this License. + + c) If the modified program normally reads commands interactively when run, + you must cause it, when started running for such interactive use in the + most ordinary way, to print or display an announcement including an + appropriate copyright notice and a notice that there is no warranty (or + else, saying that you provide a warranty) and that users may redistribute + the program under these conditions, and telling the user how to view a copy + of this License. (Exception: if the Program itself is interactive but does + not normally print such an announcement, your work based on the Program is + not required to print an announcement.) + +These requirements apply to the modified work as a whole. If identifiable +sections of that work are not derived from the Program, and can be reasonably +considered independent and separate works in themselves, then this License, and +its terms, do not apply to those sections when you distribute them as separate +works. But when you distribute the same sections as part of a whole which is a +work based on the Program, the distribution of the whole must be on the terms +of this License, whose permissions for other licensees extend to the entire +whole, and thus to each and every part regardless of who wrote it. + +Thus, it is not the intent of this section to claim rights or contest your +rights to work written entirely by you; rather, the intent is to exercise the +right to control the distribution of derivative or collective works based on +the Program. + +In addition, mere aggregation of another work not based on the Program with the +Program (or with a work based on the Program) on a volume of a storage or +distribution medium does not bring the other work under the scope of this +License. + +3. You may copy and distribute the Program (or a work based on it, under +Section 2) in object code or executable form under the terms of Sections 1 and +2 above provided that you also do one of the following: + + a) Accompany it with the complete corresponding machine-readable source + code, which must be distributed under the terms of Sections 1 and 2 above + on a medium customarily used for software interchange; or, + + b) Accompany it with a written offer, valid for at least three years, to + give any third party, for a charge no more than your cost of physically + performing source distribution, a complete machine-readable copy of the + corresponding source code, to be distributed under the terms of Sections 1 + and 2 above on a medium customarily used for software interchange; or, + + c) Accompany it with the information you received as to the offer to + distribute corresponding source code. (This alternative is allowed only + for noncommercial distribution and only if you received the program in + object code or executable form with such an offer, in accord with + Subsection b above.) + +The source code for a work means the preferred form of the work for making +modifications to it. For an executable work, complete source code means all +the source code for all modules it contains, plus any associated interface +definition files, plus the scripts used to control compilation and installation +of the executable. However, as a special exception, the source code +distributed need not include anything that is normally distributed (in either +source or binary form) with the major components (compiler, kernel, and so on) +of the operating system on which the executable runs, unless that component +itself accompanies the executable. + +If distribution of executable or object code is made by offering access to copy +from a designated place, then offering equivalent access to copy the source +code from the same place counts as distribution of the source code, even though +third parties are not compelled to copy the source along with the object code. + +4. You may not copy, modify, sublicense, or distribute the Program except as +expressly provided under this License. Any attempt otherwise to copy, modify, +sublicense or distribute the Program is void, and will automatically terminate +your rights under this License. However, parties who have received copies, or +rights, from you under this License will not have their licenses terminated so +long as such parties remain in full compliance. + +5. You are not required to accept this License, since you have not signed it. +However, nothing else grants you permission to modify or distribute the Program +or its derivative works. These actions are prohibited by law if you do not +accept this License. Therefore, by modifying or distributing the Program (or +any work based on the Program), you indicate your acceptance of this License to +do so, and all its terms and conditions for copying, distributing or modifying +the Program or works based on it. + +6. Each time you redistribute the Program (or any work based on the Program), +the recipient automatically receives a license from the original licensor to +copy, distribute or modify the Program subject to these terms and conditions. +You may not impose any further restrictions on the recipients' exercise of the +rights granted herein. You are not responsible for enforcing compliance by +third parties to this License. + +7. If, as a consequence of a court judgment or allegation of patent +infringement or for any other reason (not limited to patent issues), 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 distribute so as to satisfy +simultaneously your obligations under this License and any other pertinent +obligations, then as a consequence you may not distribute the Program at all. +For example, if a patent license would not permit royalty-free redistribution +of the Program by all those who receive copies directly or indirectly through +you, then the only way you could satisfy both it and this License would be to +refrain entirely from distribution of the Program. + +If any portion of this section is held invalid or unenforceable under any +particular circumstance, the balance of the section is intended to apply and +the section as a whole is intended to apply in other circumstances. + +It is not the purpose of this section to induce you to infringe any patents or +other property right claims or to contest validity of any such claims; this +section has the sole purpose of protecting the integrity of the free software +distribution system, which is implemented by public license practices. Many +people have made generous contributions to the wide range of software +distributed through that system in reliance on consistent application of that +system; it is up to the author/donor to decide if he or she is willing to +distribute software through any other system and a licensee cannot impose that +choice. + +This section is intended to make thoroughly clear what is believed to be a +consequence of the rest of this License. + +8. If the distribution and/or use of the Program is restricted in certain +countries either by patents or by copyrighted interfaces, the original +copyright holder who places the Program under this License may add an explicit +geographical distribution limitation excluding those countries, so that +distribution is permitted only in or among countries not thus excluded. In +such case, this License incorporates the limitation as if written in the body +of this License. + +9. The Free Software Foundation may publish revised and/or new versions of the +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 a version number of this License which applies to it and "any later +version", you have the option of following the terms and conditions either of +that version or of any later version published by the Free Software Foundation. +If the Program does not specify a version number of this License, you may +choose any version ever published by the Free Software Foundation. + +10. If you wish to incorporate parts of the Program into other free programs +whose distribution conditions are different, write to the author to ask for +permission. For software which is copyrighted by the Free Software Foundation, +write to the Free Software Foundation; we sometimes make exceptions for this. +Our decision will be guided by the two goals of preserving the free status of +all derivatives of our free software and of promoting the sharing and reuse of +software generally. + +NO WARRANTY + +11. BECAUSE THE PROGRAM IS LICENSED FREE OF CHARGE, 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. + +12. IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING WILL +ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MAY MODIFY AND/OR REDISTRIBUTE 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. + +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 convey the exclusion +of warranty; and each file should have at least the "copyright" line and a +pointer to where the full notice is found. + + One line to give the program's name and a brief idea of what it does. + + Copyright (C) + + 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 2 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, write to the Free Software Foundation, Inc., + 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. + +Also add information on how to contact you by electronic and paper mail. + +If the program is interactive, make it output a short notice like this when it +starts in an interactive mode: + + Gnomovision version 69, Copyright (C) year name of author Gnomovision 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, the commands you use may be +called something other than 'show w' and 'show c'; they could even be +mouse-clicks or menu items--whatever suits your program. + +You should also get your employer (if you work as a programmer) or your school, +if any, to sign a "copyright disclaimer" for the program, if necessary. Here +is a sample; alter the names: + + Yoyodyne, Inc., hereby disclaims all copyright interest in the program + 'Gnomovision' (which makes passes at compilers) written by James Hacker. + + signature of Ty Coon, 1 April 1989 + + Ty Coon, President of Vice + +This 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 Library General Public +License instead of this License. + + +"CLASSPATH" EXCEPTION TO THE GPL + +Certain source files distributed by Oracle America and/or its affiliates are +subject to the following clarification and special exception to the GPL, but +only where Oracle has expressly included in the particular source file's header +the words "Oracle designates this particular file as subject to the "Classpath" +exception as provided by Oracle in the LICENSE file that accompanied this code." + + Linking this library statically or dynamically with other modules is making + a combined work based on this library. Thus, the terms and conditions of + the GNU General Public License cover the whole combination. + + As a special exception, the copyright holders of this library give you + permission to link this library with independent modules to produce an + executable, regardless of the license terms of these independent modules, + and to copy and distribute the resulting executable under terms of your + choice, provided that you also meet, for each linked independent module, + the terms and conditions of the license of that module. An independent + module is a module which is not derived from or based on this library. If + you modify this library, you may extend this exception to your version of + the library, but you are not obligated to do so. If you do not wish to do + so, delete this exception statement from your version. diff --git a/target/apidocs/legal/jquery.md b/target/site/apidocs/legal/jquery.md similarity index 100% rename from target/apidocs/legal/jquery.md rename to target/site/apidocs/legal/jquery.md diff --git a/target/apidocs/legal/jqueryUI.md b/target/site/apidocs/legal/jqueryUI.md similarity index 100% rename from target/apidocs/legal/jqueryUI.md rename to target/site/apidocs/legal/jqueryUI.md diff --git a/target/apidocs/link.svg b/target/site/apidocs/link.svg similarity index 100% rename from target/apidocs/link.svg rename to target/site/apidocs/link.svg diff --git a/target/apidocs/member-search-index.js b/target/site/apidocs/member-search-index.js similarity index 100% rename from target/apidocs/member-search-index.js rename to target/site/apidocs/member-search-index.js diff --git a/target/apidocs/module-search-index.js b/target/site/apidocs/module-search-index.js similarity index 100% rename from target/apidocs/module-search-index.js rename to target/site/apidocs/module-search-index.js diff --git a/target/apidocs/overview-summary.html b/target/site/apidocs/overview-summary.html similarity index 87% rename from target/apidocs/overview-summary.html rename to target/site/apidocs/overview-summary.html index 446dc34..02bb264 100644 --- a/target/apidocs/overview-summary.html +++ b/target/site/apidocs/overview-summary.html @@ -1,11 +1,11 @@ - + Vision Skills Progression Tracker 1.0.0-beta API - + diff --git a/target/apidocs/overview-tree.html b/target/site/apidocs/overview-tree.html similarity index 99% rename from target/apidocs/overview-tree.html rename to target/site/apidocs/overview-tree.html index b54a0cb..d045ec4 100644 --- a/target/apidocs/overview-tree.html +++ b/target/site/apidocs/overview-tree.html @@ -1,11 +1,11 @@ - + Class Hierarchy (Vision Skills Progression Tracker 1.0.0-beta API) - + diff --git a/target/apidocs/package-search-index.js b/target/site/apidocs/package-search-index.js similarity index 100% rename from target/apidocs/package-search-index.js rename to target/site/apidocs/package-search-index.js diff --git a/target/apidocs/package-summary.html b/target/site/apidocs/package-summary.html similarity index 96% rename from target/apidocs/package-summary.html rename to target/site/apidocs/package-summary.html index 5ea4b43..b36a68b 100644 --- a/target/apidocs/package-summary.html +++ b/target/site/apidocs/package-summary.html @@ -1,11 +1,11 @@ - + Unnamed Package (Vision Skills Progression Tracker 1.0.0-beta API) - + diff --git a/target/apidocs/package-tree.html b/target/site/apidocs/package-tree.html similarity index 96% rename from target/apidocs/package-tree.html rename to target/site/apidocs/package-tree.html index 22d78c7..9302ec2 100644 --- a/target/apidocs/package-tree.html +++ b/target/site/apidocs/package-tree.html @@ -1,11 +1,11 @@ - + Class Hierarchy (Vision Skills Progression Tracker 1.0.0-beta API) - + diff --git a/target/apidocs/package-use.html b/target/site/apidocs/package-use.html similarity index 95% rename from target/apidocs/package-use.html rename to target/site/apidocs/package-use.html index 1994e0f..cd8a5c0 100644 --- a/target/apidocs/package-use.html +++ b/target/site/apidocs/package-use.html @@ -1,11 +1,11 @@ - + Uses of Package (Vision Skills Progression Tracker 1.0.0-beta API) - + diff --git a/target/apidocs/resources/glass.png b/target/site/apidocs/resources/glass.png similarity index 100% rename from target/apidocs/resources/glass.png rename to target/site/apidocs/resources/glass.png diff --git a/target/apidocs/resources/x.png b/target/site/apidocs/resources/x.png similarity index 100% rename from target/apidocs/resources/x.png rename to target/site/apidocs/resources/x.png diff --git a/target/apidocs/script-dir/jquery-3.7.1.min.js b/target/site/apidocs/script-dir/jquery-3.7.1.min.js similarity index 100% rename from target/apidocs/script-dir/jquery-3.7.1.min.js rename to target/site/apidocs/script-dir/jquery-3.7.1.min.js diff --git a/target/apidocs/script-dir/jquery-ui.min.css b/target/site/apidocs/script-dir/jquery-ui.min.css similarity index 100% rename from target/apidocs/script-dir/jquery-ui.min.css rename to target/site/apidocs/script-dir/jquery-ui.min.css diff --git a/target/apidocs/script-dir/jquery-ui.min.js b/target/site/apidocs/script-dir/jquery-ui.min.js similarity index 100% rename from target/apidocs/script-dir/jquery-ui.min.js rename to target/site/apidocs/script-dir/jquery-ui.min.js diff --git a/target/apidocs/script.js b/target/site/apidocs/script.js similarity index 100% rename from target/apidocs/script.js rename to target/site/apidocs/script.js diff --git a/target/apidocs/search-page.js b/target/site/apidocs/search-page.js similarity index 100% rename from target/apidocs/search-page.js rename to target/site/apidocs/search-page.js diff --git a/target/apidocs/search.html b/target/site/apidocs/search.html similarity index 96% rename from target/apidocs/search.html rename to target/site/apidocs/search.html index 22e2b69..454976d 100644 --- a/target/apidocs/search.html +++ b/target/site/apidocs/search.html @@ -1,11 +1,11 @@ - + Search (Vision Skills Progression Tracker 1.0.0-beta API) - + diff --git a/target/apidocs/search.js b/target/site/apidocs/search.js similarity index 100% rename from target/apidocs/search.js rename to target/site/apidocs/search.js diff --git a/target/apidocs/serialized-form.html b/target/site/apidocs/serialized-form.html similarity index 99% rename from target/apidocs/serialized-form.html rename to target/site/apidocs/serialized-form.html index 5c9ca44..2585372 100644 --- a/target/apidocs/serialized-form.html +++ b/target/site/apidocs/serialized-form.html @@ -1,11 +1,11 @@ - + Serialized Form (Vision Skills Progression Tracker 1.0.0-beta API) - + diff --git a/target/site/apidocs/src-html/VersionUtil.html b/target/site/apidocs/src-html/VersionUtil.html new file mode 100644 index 0000000..fbe0ce4 --- /dev/null +++ b/target/site/apidocs/src-html/VersionUtil.html @@ -0,0 +1,147 @@ + + + + +Source code + + + + + + +
    +
    +
    001import java.io.IOException;
    +002import java.io.InputStream;
    +003import java.util.Properties;
    +004
    +005import org.slf4j.Logger;
    +006import org.slf4j.LoggerFactory;
    +007
    +008/**
    +009 * Utility to surface project version information.
    +010 *
    +011 * Reads the {@code /version.properties} file from the classpath and exposes
    +012 * the {@link #getVersion()} helper. If the file cannot be read, returns
    +013 * {@code "unknown"}.
    +014 */
    +015public class VersionUtil {
    +016    private static final Logger LOG = LoggerFactory.getLogger(VersionUtil.class);
    +017
    +018    /** The path to the properties file containing the version information. */
    +019    private static final String VERSION_FILE = "/version.properties";
    +020
    +021    /** The version of the application, initialized from the properties file. */
    +022    private static String version;
    +023
    +024    /**
    +025     * Static block to initialize the {@link #version} variable.
    +026     * <p>
    +027     * The static block loads the version from the {@code version.properties} file.
    +028     * If the file cannot be found or an I/O error occurs, the version is set to "unknown".
    +029     * </p>
    +030     */
    +031    static {
    +032        try (InputStream input = VersionUtil.class.getResourceAsStream(VERSION_FILE)) {
    +033            Properties properties = new Properties();
    +034                if (input == null) {
    +035                    // If the properties file is not found, set version to "unknown"
    +036                    LOG.warn("Unable to find {}", VERSION_FILE);
    +037                    version = "unknown";
    +038                } else {
    +039                // Load the properties file and set the version
    +040                properties.load(input);
    +041                version = properties.getProperty("version", "unknown");
    +042            }
    +043        } catch (IOException ex) {
    +044            // Log the exception and set version to "unknown" in case of an exception
    +045            LOG.error("Error reading version properties", ex);
    +046            version = "unknown";
    +047        }
    +048    }
    +049
    +050    /**
    +051     * Returns the version of the application.
    +052     * <p>
    +053     * This method provides access to the version information that was loaded from the properties file.
    +054     * If the properties file could not be found or an error occurred, it returns "unknown".
    +055     * </p>
    +056     *
    +057     * @return The version of the application.
    +058     */
    +059    public static String getVersion() {
    +060        return version;
    +061    }
    +062
    +063    /**
    +064     * Private constructor to prevent instantiation of this utility class.
    +065     */
    +066    private VersionUtil() {
    +067        throw new AssertionError("Not instantiable");
    +068    }
    +069}
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    + + diff --git a/target/site/apidocs/src-html/com/studentgui/app/DateChangeListener.html b/target/site/apidocs/src-html/com/studentgui/app/DateChangeListener.html new file mode 100644 index 0000000..ed59b18 --- /dev/null +++ b/target/site/apidocs/src-html/com/studentgui/app/DateChangeListener.html @@ -0,0 +1,93 @@ + + + + +Source code + + + + + + +
    +
    +
    001package com.studentgui.app;
    +002
    +003import java.time.LocalDate;
    +004
    +005/**
    +006 * Simple listener interface for pages that want to be notified when the
    +007 * application-wide selected date changes via the top-bar Apply action.
    +008 */
    +009public interface DateChangeListener {
    +010    /**
    +011     * Called when the application date has been changed by the user.
    +012     * @param newDate the newly selected date
    +013     */
    +014    void dateChanged(LocalDate newDate);
    +015}
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    + + diff --git a/target/site/apidocs/src-html/com/studentgui/app/Main.html b/target/site/apidocs/src-html/com/studentgui/app/Main.html new file mode 100644 index 0000000..dcdf5e0 --- /dev/null +++ b/target/site/apidocs/src-html/com/studentgui/app/Main.html @@ -0,0 +1,675 @@ + + + + +Source code + + + + + + +
    +
    +
    001package com.studentgui.app;
    +002
    +003import java.awt.BorderLayout;
    +004import java.awt.CardLayout;
    +005import java.awt.FlowLayout;
    +006import java.time.LocalDate;
    +007import java.time.format.DateTimeParseException;
    +008import java.util.List;
    +009
    +010import javax.swing.JButton;
    +011import javax.swing.JComboBox;
    +012import javax.swing.JComponent;
    +013import javax.swing.JFrame;
    +014import javax.swing.JLabel;
    +015import javax.swing.JPanel;
    +016import javax.swing.JTextField;
    +017import javax.swing.SwingUtilities;
    +018import javax.swing.UIManager;
    +019
    +020import org.slf4j.Logger;
    +021import org.slf4j.LoggerFactory;
    +022
    +023import com.studentgui.apphelpers.Helpers;
    +024import com.studentgui.apphelpers.SqlGenerate;
    +025import com.studentgui.apppages.Abacus;
    +026import com.studentgui.apppages.Braille;
    +027import com.studentgui.apppages.BrailleNote;
    +028import com.studentgui.apppages.BrailleSense;
    +029import com.studentgui.apppages.CVI;
    +030import com.studentgui.apppages.ContactLog;
    +031import com.studentgui.apppages.DigitalLiteracy;
    +032import com.studentgui.apppages.Homepage;
    +033import com.studentgui.apppages.IOS;
    +034import com.studentgui.apppages.InstructionalMaterials;
    +035import com.studentgui.apppages.JLineGraph;
    +036import com.studentgui.apppages.Keyboarding;
    +037import com.studentgui.apppages.Observations;
    +038import com.studentgui.apppages.ScreenReader;
    +039import com.studentgui.apppages.SessionNotes;
    +040import com.studentgui.apptheming.Theme;
    +041
    +042/**
    +043 * Main application entry and UI wiring for the Student Skills Progressions app.
    +044 *
    +045 * This class builds the top-level window, menu, and registers the skill pages
    +046 * (each page is a JPanel). It's intentionally lightweight; most functionality
    +047 * for database access and page logic lives in helper classes and the page
    +048 * components under com.studentgui.apppages.
    +049 */
    +050/**
    +051 * Application bootstrap and top-level UI wiring. Builds the main JFrame,
    +052 * registers pages, and provides a small top control bar for switching
    +053 * students and pages.
    +054 */
    +055/**
    +056 * Application bootstrap and top-level UI wiring. Builds the main JFrame,
    +057 * registers pages, and provides a small top control bar for switching
    +058 * students and pages.
    +059 */
    +060/**
    +061 * Application entry point and top-level UI wiring for the Student Skills
    +062 * Progressions application. Builds the main frame, menu and registers per-page
    +063 * panels under a CardLayout.
    +064 */
    +065public class Main {
    +066    /**
    +067     * Bootstrap logging/system properties very early so Logback can resolve
    +068     * file locations and the per-run timestamp before any logger is
    +069     * initialized. This static block sets APP_HOME and LOG_TS and performs
    +070     * a cleanup of old log files older than 7 days.
    +071     */
    +072    static {
    +073        try {
    +074            // Ensure Helpers.APP_HOME is initialized and use it for logging
    +075            String appHome = com.studentgui.apphelpers.Helpers.APP_HOME.toString();
    +076            System.setProperty("APP_HOME", appHome);
    +077            // unix epoch seconds appended to per-run log filename
    +078            String ts = String.valueOf(java.time.Instant.now().getEpochSecond());
    +079            System.setProperty("LOG_TS", ts);
    +080
    +081            // create logs dir
    +082            java.nio.file.Path logs = java.nio.file.Paths.get(appHome).resolve("logs");
    +083            java.nio.file.Files.createDirectories(logs);
    +084
    +085            // Cleanup: remove log files older than 7 days (by last modified time)
    +086            long cutoff = java.time.Instant.now().minus(java.time.Duration.ofDays(7)).toEpochMilli();
    +087            try (java.nio.file.DirectoryStream<java.nio.file.Path> ds = java.nio.file.Files.newDirectoryStream(logs, "log_*.log")) {
    +088                for (java.nio.file.Path p : ds) {
    +089                    try {
    +090                        java.nio.file.attribute.FileTime ft = java.nio.file.Files.getLastModifiedTime(p);
    +091                        if (ft.toMillis() < cutoff) {
    +092                            java.nio.file.Files.deleteIfExists(p);
    +093                        }
    +094                    } catch (Exception ex) {
    +095                        // Swallow cleanup exceptions; logging isn't available yet.
    +096                    }
    +097                }
    +098            } catch (Exception ex) {
    +099                // ignore
    +100            }
    +101            // also remove consolidated data dump files older than retention
    +102            try (java.nio.file.DirectoryStream<java.nio.file.Path> ds2 = java.nio.file.Files.newDirectoryStream(logs, "data_dumps_*.log")) {
    +103                for (java.nio.file.Path p : ds2) {
    +104                    try {
    +105                        java.nio.file.attribute.FileTime ft = java.nio.file.Files.getLastModifiedTime(p);
    +106                        if (ft.toMillis() < cutoff) {
    +107                            java.nio.file.Files.deleteIfExists(p);
    +108                        }
    +109                    } catch (Exception ex) {
    +110                        // Swallow cleanup exceptions; logging isn't available yet.
    +111                    }
    +112                }
    +113            } catch (Exception ex) {
    +114                // ignore
    +115            }
    +116        } catch (Exception ex) {
    +117            // If anything here fails, continue — logging may not be configured yet.
    +118        }
    +119    }
    +120
    +121    private static final Logger LOG = LoggerFactory.getLogger(Main.class);
    +122    private static JFrame frame;
    +123    private static JPanel contentPanel;
    +124    private static JLineGraph sharedGraph;
    +125    /**
    +126     * Shared JLineGraph instance used across pages.
    +127     *
    +128     * Pages are constructed with the shared graph passed into their
    +129     * constructors (see recreatePages). The shared graph is registered
    +130     * with {@link #addSettingsChangeListener(SettingsChangeListener)} so
    +131     * it receives runtime preference updates. If a page creates its own
    +132     * page-local JLineGraph instance it should register it with
    +133     * {@link #addSettingsChangeListener(SettingsChangeListener)} and remove
    +134     * it when disposed to ensure it receives preference changes and to
    +135     * avoid leaking listeners.
    +136     */
    +137    // current date used by the top bar (can be updated without recreating pages)
    +138    private static java.time.LocalDate currentDate;
    +139    private static String currentStudent;
    +140    // Listeners to notify when the top-bar date changes
    +141    private static final java.util.List<DateChangeListener> dateListeners = new java.util.concurrent.CopyOnWriteArrayList<>();
    +142
    +143    /**
    +144     * Register a listener to be notified when the application date is changed via the top bar.
    +145     *
    +146     * @param l listener to register (ignored when null)
    +147     */
    +148    public static void addDateChangeListener(final DateChangeListener l) { 
    +149        if (l != null) {
    +150            dateListeners.add(l);
    +151        }
    +152    }
    +153
    +154    /**
    +155     * Remove a previously registered date change listener.
    +156     *
    +157     * @param l listener to remove (ignored when null)
    +158     */
    +159    public static void removeDateChangeListener(final DateChangeListener l) { 
    +160        if (l != null) {
    +161            dateListeners.remove(l);
    +162        }
    +163    }
    +164
    +165    /**
    +166     * Clear all registered date change listeners.
    +167     */
    +168    public static void clearDateChangeListeners() { 
    +169        dateListeners.clear();
    +170    }
    +171
    +172    /**
    +173     * Notify all registered date listeners that the application date has changed.
    +174     *
    +175     * @param d new application date
    +176     */
    +177    private static void notifyDateChanged(final java.time.LocalDate d) {
    +178        for (DateChangeListener l : dateListeners) {
    +179            try {
    +180                l.dateChanged(d);
    +181            } catch (Exception ex) {
    +182                LOG.warn("DateChangeListener threw: {}", ex.toString());
    +183            }
    +184        }
    +185    }
    +186    // Student change listeners
    +187    private static final java.util.List<StudentChangeListener> studentListeners = new java.util.concurrent.CopyOnWriteArrayList<>();
    +188    /**
    +189     * Register a listener to be notified when the selected student is changed.
    +190     *
    +191     * @param l listener to register (ignored when null)
    +192     */
    +193    public static void addStudentChangeListener(final StudentChangeListener l) {
    +194        if (l != null) {
    +195            studentListeners.add(l);
    +196        }
    +197    }
    +198
    +199    /**
    +200     * Remove a previously registered student change listener.
    +201     *
    +202     * @param l listener to remove (ignored when null)
    +203     */
    +204    public static void removeStudentChangeListener(final StudentChangeListener l) {
    +205        if (l != null) {
    +206            studentListeners.remove(l);
    +207        }
    +208    }
    +209
    +210    /**
    +211     * Clear all registered student change listeners.
    +212     */
    +213    public static void clearStudentChangeListeners() {
    +214        studentListeners.clear();
    +215    }
    +216
    +217    /**
    +218     * Notify registered student change listeners that the selected student has changed.
    +219     *
    +220     * @param s new selected student name
    +221     */
    +222    private static void notifyStudentChanged(final String s) {
    +223        currentStudent = s;
    +224        for (StudentChangeListener l : studentListeners) {
    +225            try {
    +226                l.studentChanged(s);
    +227            } catch (Exception ex) {
    +228                LOG.warn("StudentChangeListener threw: {}", ex.toString());
    +229            }
    +230        }
    +231    }
    +232
    +233
    +234    // Settings change listeners
    +235    private static final java.util.List<SettingsChangeListener> settingsListeners = new java.util.concurrent.CopyOnWriteArrayList<>();
    +236
    +237    /**
    +238     * Register a listener to be notified when application settings change.
    +239     * Implementations should read values from {@link com.studentgui.apphelpers.Settings}
    +240     * when {@link SettingsChangeListener#settingsChanged()} is invoked.
    +241     *
    +242     * @param l listener to register (ignored when null)
    +243     */
    +244    public static void addSettingsChangeListener(final SettingsChangeListener l) {
    +245        if (l != null) {
    +246            settingsListeners.add(l);
    +247        }
    +248    }
    +249
    +250    /**
    +251     * Remove a previously registered settings change listener.
    +252     *
    +253     * @param l listener to remove (ignored when null)
    +254     */
    +255    public static void removeSettingsChangeListener(final SettingsChangeListener l) {
    +256        if (l != null) {
    +257            settingsListeners.remove(l);
    +258        }
    +259    }
    +260
    +261    /**
    +262     * Clear all registered settings change listeners.
    +263     */
    +264    public static void clearSettingsChangeListeners() {
    +265        settingsListeners.clear();
    +266    }
    +267
    +268    /**
    +269     * Notify all registered settings listeners that application settings have been changed.
    +270     * This is typically invoked after persisting preferences through
    +271     * {@link com.studentgui.apphelpers.Settings}.
    +272     */
    +273    public static void notifySettingsChanged() {
    +274        for (SettingsChangeListener l : settingsListeners) {
    +275            try {
    +276                l.settingsChanged();
    +277            } catch (Exception ex) {
    +278                LOG.warn("SettingsChangeListener threw: {}", ex.toString());
    +279            }
    +280        }
    +281    }
    +282
    +283    /**
    +284     * Application entry point. Initializes helpers, database, and launches the
    +285     * Swing UI on the EDT.
    +286     *
    +287     * @param args command-line arguments (unused)
    +288     */
    +289    public static void main(final String[] args) {
    +290        // Apply saved look and feel (default to light)
    +291        // Settings.get and setTheme handle any expected failures internally;
    +292        // call directly so we avoid a broad RuntimeException catch.
    +293        String saved = com.studentgui.apphelpers.Settings.get("theme", "light");
    +294        setTheme(saved);
    +295
    +296        // Initialize helpers and DB
    +297        Helpers.setStartDir();
    +298        Helpers.createFolderHierarchy();
    +299        SqlGenerate.initializeDatabase();
    +300
    +301        SwingUtilities.invokeLater(() -> {
    +302            frame = new JFrame("Student Skills Progressions");
    +303            frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
    +304            frame.setSize(1000, 700);
    +305            frame.setLocationRelativeTo(null);
    +306
    +307            // Menu bar: obtain the app menu bar from Theme, insert a File->Exit menu at the far left
    +308            javax.swing.JMenuBar themeBar = Theme.createMenuBar();
    +309            if (themeBar == null) {
    +310                themeBar = new javax.swing.JMenuBar();
    +311            }
    +312            javax.swing.JMenu fileMenu = new javax.swing.JMenu("File");
    +313            javax.swing.JMenuItem exitItem = new javax.swing.JMenuItem("Exit");
    +314            exitItem.addActionListener(e -> {
    +315                LOG.info("Exit requested via File->Exit");
    +316                if (frame != null) {
    +317                    frame.dispose();
    +318                }
    +319                System.exit(0);
    +320            });
    +321            fileMenu.add(exitItem);
    +322            // Insert file menu at position 0 so it appears on the far left
    +323            themeBar.add(fileMenu, 0);
    +324            // Ensure the Themes menu (if present) appears immediately after File
    +325            int themesIdx = -1;
    +326            for (int i = 0; i < themeBar.getMenuCount(); i++) {
    +327                javax.swing.JMenu m = themeBar.getMenu(i);
    +328                if (m != null && "Themes".equals(m.getText())) { themesIdx = i; break; }
    +329            }
    +330            if (themesIdx > 1) {
    +331                javax.swing.JMenu themesMenu = themeBar.getMenu(themesIdx);
    +332                themeBar.remove(themesIdx);
    +333                themeBar.add(themesMenu, 1);
    +334            }
    +335            frame.setJMenuBar(themeBar);
    +336
    +337
    +338            contentPanel = new JPanel(new CardLayout());
    +339            frame.add(contentPanel, BorderLayout.CENTER);
    +340
    +341            // Top control bar: student selector, date, and navigation
    +342            JPanel topBar = buildTopBar();
    +343            frame.add(topBar, BorderLayout.NORTH);
    +344
    +345            // Create initial shared graph and pages for the first student
    +346            sharedGraph = new JLineGraph();
    +347            // Register shared graph to receive settings change notifications
    +348            addSettingsChangeListener(sharedGraph);
    +349            List<String> students = Helpers.getStudents();
    +350            String demoStudent = students.isEmpty() ? "Demo Student" : students.get(0);
    +351            LocalDate today = LocalDate.now();
    +352            currentDate = today;
    +353            recreatePages(demoStudent, today);
    +354
    +355            frame.setVisible(true);
    +356        });
    +357    }
    +358
    +359    /**
    +360     * Change application theme at runtime. Supported values: "light", "dark", "darcula".
    +361     * This method updates the installed Look and Feel and refreshes the main frame.
    +362    *
    +363    * @param theme human-friendly theme name or fully-qualified LookAndFeel class name
    +364     */
    +365    public static void setTheme(final String theme) {
    +366        try {
    +367            String t = theme == null ? "light" : theme;
    +368            // Common keywords for bundled themes
    +369            switch (t.toLowerCase()) {
    +370                case "dark":
    +371                case "flatdarklaf":
    +372                    UIManager.setLookAndFeel(new com.formdev.flatlaf.FlatDarkLaf());
    +373                    break;
    +374                case "darcula":
    +375                    // Darcula-like: use FlatDarkLaf as fallback
    +376                    UIManager.setLookAndFeel(new com.formdev.flatlaf.FlatDarkLaf());
    +377                    break;
    +378                case "light":
    +379                case "flatlightlaf":
    +380                    UIManager.setLookAndFeel(new com.formdev.flatlaf.FlatLightLaf());
    +381                    break;
    +382                default:
    +383                    // If the string looks like a fully-qualified class name, try to set it directly.
    +384                    if (t.contains(".")) {
    +385                        try {
    +386                            UIManager.setLookAndFeel(t);
    +387                        } catch (ReflectiveOperationException | javax.swing.UnsupportedLookAndFeelException ex) {
    +388                            // Try to instantiate via reflection
    +389                            try {
    +390                                Class<?> c = Class.forName(t);
    +391                                Object o = c.getDeclaredConstructor().newInstance();
    +392                                if (o instanceof javax.swing.LookAndFeel) {
    +393                                    UIManager.setLookAndFeel((javax.swing.LookAndFeel) o);
    +394                                } else {
    +395                                    // fallback to light
    +396                                    UIManager.setLookAndFeel(new com.formdev.flatlaf.FlatLightLaf());
    +397                                }
    +398                            } catch (ReflectiveOperationException | javax.swing.UnsupportedLookAndFeelException ex2) {
    +399                                LOG.error("Failed to set look and feel by class name {}", t, ex2);
    +400                                UIManager.setLookAndFeel(new com.formdev.flatlaf.FlatLightLaf());
    +401                            }
    +402                        }
    +403                    } else {
    +404                        // Try to find an installed LAF by name
    +405                        boolean applied = false;
    +406                        for (UIManager.LookAndFeelInfo info : UIManager.getInstalledLookAndFeels()) {
    +407                            if (info.getName().equalsIgnoreCase(t) || info.getName().toLowerCase().contains(t.toLowerCase())) {
    +408                                UIManager.setLookAndFeel(info.getClassName());
    +409                                applied = true;
    +410                                break;
    +411                            }
    +412                        }
    +413                        if (!applied) {
    +414                            // default fallback
    +415                            UIManager.setLookAndFeel(new com.formdev.flatlaf.FlatLightLaf());
    +416                        }
    +417                    }
    +418                    break;
    +419            }
    +420            if (frame != null) {
    +421                javax.swing.SwingUtilities.updateComponentTreeUI(frame);
    +422                frame.pack();
    +423            }
    +424        } catch (ReflectiveOperationException | javax.swing.UnsupportedLookAndFeelException | IllegalArgumentException e) {
    +425            LOG.error("Failed to set theme {}", theme, e);
    +426        }
    +427    }
    +428
    +429    private static JPanel buildTopBar() {
    +430        JPanel bar = new JPanel(new FlowLayout(FlowLayout.LEFT));
    +431        List<String> students = Helpers.getStudents();
    +432        JComboBox<String> studentBox = new JComboBox<>(students.toArray(new String[0]));
    +433        studentBox.setEditable(false);
    +434
    +435        JLabel dateLabel = new JLabel("Date (YYYY-MM-DD):");
    +436        JTextField dateField = new JTextField(LocalDate.now().toString(), 10);
    +437
    +438        JButton goBtn = new JButton("Apply");
    +439
    +440        bar.add(new JLabel("Student:"));
    +441        bar.add(studentBox);
    +442        bar.add(dateLabel);
    +443        bar.add(dateField);
    +444        bar.add(goBtn);
    +445
    +446            goBtn.addActionListener(e -> {
    +447            String selected = (String) studentBox.getSelectedItem();
    +448            LocalDate date = LocalDate.now();
    +449            try {
    +450                date = LocalDate.parse(dateField.getText());
    +451            } catch (DateTimeParseException ex) {
    +452                // keep today
    +453            }
    +454            // Update the app's current date and selected student without recreating pages; show a confirmation dialog.
    +455            currentDate = date;
    +456            currentStudent = selected;
    +457            javax.swing.JOptionPane.showMessageDialog(frame,
    +458                    "The date has been updated to " + date.toString(),
    +459                    "Date Updated",
    +460                    javax.swing.JOptionPane.INFORMATION_MESSAGE);
    +461            // Notify registered pages so they can update any internal state
    +462            notifyDateChanged(date);
    +463            notifyStudentChanged(selected);
    +464        });
    +465
    +466    // Navigation buttons removed from top bar per UI request; pages can still be selected via menu
    +467
    +468        return bar;
    +469    }
    +470
    +471    /**
    +472     * Recreate per-page panels for the provided student and date. This replaces
    +473     * the CardLayout content so the shared graph and pages are reset.
    +474     */
    +475    /**
    +476     * Recreate per-page panels for the provided student and date. This
    +477     * replaces the CardLayout content so the shared graph and pages are reset.
    +478     *
    +479     * @param student selected student's display name
    +480     * @param date the session date for newly created pages
    +481     */
    +482    private static void recreatePages(final String student, final LocalDate date) {
    +483        // recreate the pages with a fresh sharedGraph so the graph is reset for the selected student/date
    +484        if (sharedGraph == null) {
    +485            sharedGraph = new JLineGraph();
    +486        } else {
    +487            sharedGraph = new JLineGraph();
    +488        }
    +489
    +490    // Clear any previous listeners to avoid stale references
    +491    clearDateChangeListeners();
    +492    clearStudentChangeListeners();
    +493
    +494        contentPanel.removeAll();
    +495        contentPanel.add(Homepage.create(), "homepage");
    +496
    +497        // Instantiate pages into locals so we can register listeners if they implement the interface
    +498        Braille braille = new Braille(student, date, sharedGraph);
    +499        contentPanel.add(braille, "braille");
    +500    if (braille instanceof DateChangeListener d) {
    +501        addDateChangeListener(d);
    +502    }
    +503    if (braille instanceof StudentChangeListener s) {
    +504        addStudentChangeListener(s);
    +505    }
    +506
    +507        Abacus abacus = new Abacus(student, date, sharedGraph);
    +508        contentPanel.add(abacus, "abacus");
    +509    if (abacus instanceof DateChangeListener d2) {
    +510        addDateChangeListener(d2);
    +511    }
    +512    if (abacus instanceof StudentChangeListener s2) {
    +513        addStudentChangeListener(s2);
    +514    }
    +515
    +516        BrailleNote brailleNote = new BrailleNote(student, date, sharedGraph);
    +517        contentPanel.add(brailleNote, "braillenote");
    +518    if (brailleNote instanceof DateChangeListener d3) {
    +519        addDateChangeListener(d3);
    +520    }
    +521    if (brailleNote instanceof StudentChangeListener s3) {
    +522        addStudentChangeListener(s3);
    +523    }
    +524
    +525        DigitalLiteracy dl = new DigitalLiteracy(student, date, sharedGraph);
    +526        contentPanel.add(dl, "digitalliteracy");
    +527    if (dl instanceof DateChangeListener d4) {
    +528        addDateChangeListener(d4);
    +529    }
    +530    if (dl instanceof StudentChangeListener s4) {
    +531        addStudentChangeListener(s4);
    +532    }
    +533
    +534        // pages that don't currently need date-driven updates remain created inline
    +535        contentPanel.add(new BrailleSense(student, date, sharedGraph), "braillesense");
    +536        contentPanel.add(new CVI(student, date, sharedGraph), "cvi");
    +537
    +538        IOS ios = new IOS(student, date, sharedGraph);
    +539        contentPanel.add(ios, "ios");
    +540    if (ios instanceof DateChangeListener d5) {
    +541        addDateChangeListener(d5);
    +542    }
    +543    if (ios instanceof StudentChangeListener s5) {
    +544        addStudentChangeListener(s5);
    +545    }
    +546
    +547        Keyboarding keyboarding = new Keyboarding(student, date, sharedGraph);
    +548        contentPanel.add(keyboarding, "keyboarding");
    +549    if (keyboarding instanceof DateChangeListener d6) {
    +550        addDateChangeListener(d6);
    +551    }
    +552    if (keyboarding instanceof StudentChangeListener s6) {
    +553        addStudentChangeListener(s6);
    +554    }
    +555
    +556        contentPanel.add(new Observations(student, date), "observations");
    +557
    +558        ScreenReader sr = new ScreenReader(student, date, sharedGraph);
    +559        contentPanel.add(sr, "screenreader");
    +560    if (sr instanceof DateChangeListener d7) {
    +561        addDateChangeListener(d7);
    +562    }
    +563    if (sr instanceof StudentChangeListener s7) {
    +564        addStudentChangeListener(s7);
    +565    }
    +566
    +567    contentPanel.add(new SessionNotes(student, date, sharedGraph), "sessionnotes");
    +568    contentPanel.add(new ContactLog(student, date, sharedGraph), "contactlog");
    +569        contentPanel.add(new InstructionalMaterials(), "instructionalmaterials");
    +570
    +571        contentPanel.revalidate();
    +572        contentPanel.repaint();
    +573        showPage("homepage", null);
    +574    }
    +575
    +576    /**
    +577     * Show a page previously registered with the CardLayout. If a component
    +578     * is provided and not yet added it will be registered under the given name.
    +579     *
    +580     * @param name registration name for the page
    +581     * @param comp optional component instance to add (may be null)
    +582     */
    +583    public static void showPage(final String name, final JComponent comp) {
    +584        CardLayout cl = (CardLayout) contentPanel.getLayout();
    +585        if (comp != null && comp.getParent() == null) {
    +586            contentPanel.add(comp, name);
    +587        }
    +588        cl.show(contentPanel, name);
    +589    }
    +590
    +591    /**
    +592     * Private constructor to prevent instantiation of this utility/entry class.
    +593     */
    +594    private Main() {
    +595        throw new AssertionError("Not instantiable");
    +596    }
    +597}
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    + + diff --git a/target/site/apidocs/src-html/com/studentgui/app/PreferencesDialog.html b/target/site/apidocs/src-html/com/studentgui/app/PreferencesDialog.html new file mode 100644 index 0000000..6eec104 --- /dev/null +++ b/target/site/apidocs/src-html/com/studentgui/app/PreferencesDialog.html @@ -0,0 +1,155 @@ + + + + +Source code + + + + + + +
    +
    +
    001package com.studentgui.app;
    +002
    +003import java.awt.BorderLayout;
    +004import java.awt.FlowLayout;
    +005import java.awt.Frame;
    +006
    +007import javax.swing.JButton;
    +008import javax.swing.JCheckBox;
    +009import javax.swing.JDialog;
    +010import javax.swing.JLabel;
    +011import javax.swing.JPanel;
    +012import javax.swing.JTextField;
    +013
    +014import com.studentgui.apphelpers.Settings;
    +015
    +016/**
    +017 * Simple modal preferences dialog exposing a few runtime toggles that
    +018 * affect chart rendering. Preferences are persisted via
    +019 * {@link com.studentgui.apphelpers.Settings} and listeners are notified
    +020 * through {@link Main#notifySettingsChanged()}.
    +021 */
    +022public final class PreferencesDialog {
    +023    private PreferencesDialog() { throw new AssertionError(); }
    +024
    +025    /**
    +026     * Show the modal preferences dialog. The dialog persists changes to
    +027     * {@link com.studentgui.apphelpers.Settings} and notifies runtime
    +028     * listeners via {@link Main#notifySettingsChanged()}.
    +029     *
    +030     * @param owner optional parent frame for dialog positioning
    +031     */
    +032    public static void showDialog(final Frame owner) {
    +033        final JDialog dlg = new JDialog(owner, "Preferences", true);
    +034        dlg.setLayout(new BorderLayout());
    +035
    +036        JPanel center = new JPanel(new FlowLayout(FlowLayout.LEFT));
    +037        boolean jitterEnabled = Boolean.parseBoolean(Settings.get("jitter.enabled", "true"));
    +038        boolean deterministic = Boolean.parseBoolean(Settings.get("jitter.deterministic", "false"));
    +039        String seed = Settings.get("jitter.seed", "");
    +040    boolean dumpsEnabled = Boolean.parseBoolean(Settings.get("dump.enabled", "false"));
    +041
    +042        final JCheckBox jitterCb = new JCheckBox("Enable jitter", jitterEnabled);
    +043        final JCheckBox detCb = new JCheckBox("Deterministic (seeded)", deterministic);
    +044        final JTextField seedField = new JTextField(seed == null ? "" : seed, 12);
    +045    final JCheckBox dumpsCb = new JCheckBox("Enable per-page data dumps", dumpsEnabled);
    +046
    +047        center.add(jitterCb);
    +048        center.add(detCb);
    +049    center.add(dumpsCb);
    +050        center.add(new JLabel("Seed:"));
    +051        center.add(seedField);
    +052
    +053        JPanel south = new JPanel(new FlowLayout(FlowLayout.RIGHT));
    +054        JButton save = new JButton("Save");
    +055        JButton cancel = new JButton("Cancel");
    +056
    +057        save.addActionListener(e -> {
    +058            Settings.put("jitter.enabled", String.valueOf(jitterCb.isSelected()));
    +059            Settings.put("jitter.deterministic", String.valueOf(detCb.isSelected()));
    +060            Settings.put("jitter.seed", seedField.getText().trim());
    +061            Settings.put("dump.enabled", String.valueOf(dumpsCb.isSelected()));
    +062            // notify runtime listeners
    +063            Main.notifySettingsChanged();
    +064            dlg.dispose();
    +065        });
    +066
    +067        cancel.addActionListener(e -> dlg.dispose());
    +068        south.add(cancel);
    +069        south.add(save);
    +070
    +071        dlg.add(center, BorderLayout.CENTER);
    +072        dlg.add(south, BorderLayout.SOUTH);
    +073        dlg.pack();
    +074        dlg.setLocationRelativeTo(owner);
    +075        dlg.setVisible(true);
    +076    }
    +077}
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    + + diff --git a/target/site/apidocs/src-html/com/studentgui/app/SettingsChangeListener.html b/target/site/apidocs/src-html/com/studentgui/app/SettingsChangeListener.html new file mode 100644 index 0000000..14c6ef2 --- /dev/null +++ b/target/site/apidocs/src-html/com/studentgui/app/SettingsChangeListener.html @@ -0,0 +1,91 @@ + + + + +Source code + + + + + + +
    +
    +
    001package com.studentgui.app;
    +002
    +003/**
    +004 * Simple listener interface for application-wide settings changes.
    +005 */
    +006public interface SettingsChangeListener {
    +007    /**
    +008     * Invoked when application settings have been changed and persisted.
    +009     * Implementations should read the desired values from the Settings
    +010     * helper and update any runtime state accordingly.
    +011     */
    +012    void settingsChanged();
    +013}
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    + + diff --git a/target/site/apidocs/src-html/com/studentgui/app/StudentChangeListener.html b/target/site/apidocs/src-html/com/studentgui/app/StudentChangeListener.html new file mode 100644 index 0000000..e098a35 --- /dev/null +++ b/target/site/apidocs/src-html/com/studentgui/app/StudentChangeListener.html @@ -0,0 +1,90 @@ + + + + +Source code + + + + + + +
    +
    +
    001package com.studentgui.app;
    +002
    +003/**
    +004 * Listener for application-wide student selection changes.
    +005 */
    +006public interface StudentChangeListener {
    +007    /**
    +008     * Called when the application selected student has changed.
    +009     * @param newStudent the newly selected student's display name (may be null)
    +010     */
    +011    void studentChanged(String newStudent);
    +012}
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    + + diff --git a/target/site/apidocs/src-html/com/studentgui/apphelpers/Database.ResultsWithDates.html b/target/site/apidocs/src-html/com/studentgui/apphelpers/Database.ResultsWithDates.html new file mode 100644 index 0000000..dac1641 --- /dev/null +++ b/target/site/apidocs/src-html/com/studentgui/apphelpers/Database.ResultsWithDates.html @@ -0,0 +1,643 @@ + + + + +Source code + + + + + + +
    +
    +
    001package com.studentgui.apphelpers;
    +002
    +003import java.sql.Connection;
    +004import java.sql.DriverManager;
    +005import java.sql.PreparedStatement;
    +006import java.sql.ResultSet;
    +007import java.sql.SQLException;
    +008import java.sql.Statement;
    +009import java.time.LocalDate;
    +010import java.util.ArrayList;
    +011import java.util.HashMap;
    +012import java.util.List;
    +013import java.util.Map;
    +014
    +015/**
    +016 * Centralized database helper for the normalized SQLite schema.
    +017 *
    +018 * <p>Provides convenience methods to get-or-create Students and ProgressTypes,
    +019 * create ProgressSessions, ensure AssessmentParts, insert/fetch assessment
    +020 * results, and save session-specific notes. Use these helpers instead of
    +021 * running per-page DDL throughout the codebase.</p>
    +022 */
    +023public class Database {
    +024
    +025    /**
    +026     * Private constructor to prevent instantiation of this utility class.
    +027     */
    +028    private Database() {
    +029        throw new AssertionError("Database is a utility class");
    +030    }
    +031
    +032    /**
    +033     * Obtain a new JDBC Connection to the application SQLite database.
    +034     * Caller is responsible for closing the connection (try-with-resources is recommended).
    +035     *
    +036     * @return new Connection
    +037     * @throws SQLException if the driver cannot open the database
    +038     */
    +039    private static Connection getConnection() throws SQLException {
    +040        String url = "jdbc:sqlite:" + Helpers.DATABASE_PATH.toString();
    +041        return DriverManager.getConnection(url);
    +042    }
    +043    
    +044    /**
    +045     * Get a student id by name, creating a new Student row when none exists.
    +046     *
    +047     * @param name student display name
    +048     * @return id of the existing or newly created student
    +049     * @throws SQLException on database errors
    +050     */
    +051    public static int getOrCreateStudent(final String name) throws SQLException {
    +052        try (Connection c = getConnection()) {
    +053            try (PreparedStatement ps = c.prepareStatement("SELECT id FROM Student WHERE name = ?")) {
    +054                ps.setString(1, name);
    +055                try (ResultSet rs = ps.executeQuery()) {
    +056                    if (rs.next()) {
    +057                        return rs.getInt(1);
    +058                    }
    +059                }
    +060            }
    +061            try (PreparedStatement ps = c.prepareStatement("INSERT INTO Student(name) VALUES (?)", Statement.RETURN_GENERATED_KEYS)) {
    +062                ps.setString(1, name);
    +063                ps.executeUpdate();
    +064                try (ResultSet keys = ps.getGeneratedKeys()) {
    +065                    if (keys.next()) {
    +066                        return keys.getInt(1);
    +067                    }
    +068                }
    +069            }
    +070        }
    +071        throw new SQLException("Failed to create or retrieve student");
    +072    }
    +073
    +074    /**
    +075     * Get or create a ProgressType row by name.
    +076     *
    +077     * @param name progress type display name
    +078     * @return database id of the progress type
    +079     * @throws SQLException on database errors
    +080     */
    +081    public static int getOrCreateProgressType(final String name) throws SQLException {
    +082        try (Connection c = getConnection()) {
    +083            try (PreparedStatement ps = c.prepareStatement("SELECT id FROM ProgressType WHERE name = ?")) {
    +084                ps.setString(1, name);
    +085                try (ResultSet rs = ps.executeQuery()) {
    +086                    if (rs.next()) {
    +087                        return rs.getInt(1);
    +088                    }
    +089                }
    +090            }
    +091            try (PreparedStatement ps = c.prepareStatement("INSERT INTO ProgressType(name) VALUES (?)", Statement.RETURN_GENERATED_KEYS)) {
    +092                ps.setString(1, name);
    +093                ps.executeUpdate();
    +094                try (ResultSet keys = ps.getGeneratedKeys()) {
    +095                    if (keys.next()) {
    +096                        return keys.getInt(1);
    +097                    }
    +098                }
    +099            }
    +100        }
    +101        throw new SQLException("Failed to create or retrieve ProgressType");
    +102    }
    +103
    +104    /**
    +105     * Ensure AssessmentPart rows exist for the given progress type. This uses
    +106     * SQL "INSERT OR IGNORE" so existing parts are preserved.
    +107     *
    +108     * @param progressTypeId id of the ProgressType
    +109     * @param codes array of part codes to ensure
    +110     * @throws SQLException on database errors
    +111     */
    +112    public static void ensureAssessmentParts(final int progressTypeId, final String[] codes) throws SQLException {
    +113        try (Connection c = getConnection()) {
    +114            try (PreparedStatement ps = c.prepareStatement("INSERT OR IGNORE INTO AssessmentPart(progress_type_id, code, description) VALUES (?, ?, NULL)")) {
    +115                for (String code : codes) {
    +116                    ps.setInt(1, progressTypeId);
    +117                    ps.setString(2, code);
    +118                    ps.addBatch();
    +119                }
    +120                ps.executeBatch();
    +121            }
    +122        }
    +123    }
    +124
    +125    /**
    +126     * Remove any AssessmentPart rows for the given progress type whose code is
    +127     * not present in the provided canonical codes array. This helps clean up
    +128     * legacy/malformed entries that could cause part ordering mismatches.
    +129     *
    +130     * @param progressTypeId id of the ProgressType
    +131     * @param allowedCodes canonical set of codes to keep
    +132     * @throws SQLException on database errors
    +133     */
    +134    public static void cleanupAssessmentParts(final int progressTypeId, final String[] allowedCodes) throws SQLException {
    +135        if (allowedCodes == null || allowedCodes.length == 0) {
    +136            return;
    +137        }
    +138        try (Connection c = getConnection()) {
    +139            StringBuilder sb = new StringBuilder();
    +140            for (int i = 0; i < allowedCodes.length; i++) {
    +141                if (i > 0) { sb.append(','); }
    +142                sb.append('?');
    +143            }
    +144            String sql = "DELETE FROM AssessmentPart WHERE progress_type_id = ? AND code NOT IN (" + sb.toString() + ")";
    +145            try (PreparedStatement ps = c.prepareStatement(sql)) {
    +146                ps.setInt(1, progressTypeId);
    +147                for (int i = 0; i < allowedCodes.length; i++) {
    +148                    ps.setString(i + 2, allowedCodes[i]);
    +149                }
    +150                ps.executeUpdate();
    +151            }
    +152        }
    +153    }
    +154
    +155    /**
    +156     * Create a ProgressSession for a student and progress type on the given date.
    +157     *
    +158     * @param studentId existing student id
    +159     * @param progressTypeId existing progress type id
    +160     * @param date session date
    +161     * @return generated ProgressSession id
    +162     * @throws SQLException on database errors
    +163     */
    +164    public static int createProgressSession(final int studentId, final int progressTypeId, final LocalDate date) throws SQLException {
    +165        try (Connection c = getConnection()) {
    +166            try (PreparedStatement ps = c.prepareStatement("INSERT INTO ProgressSession(student_id, progress_type_id, date, notes) VALUES (?, ?, ?, NULL)", Statement.RETURN_GENERATED_KEYS)) {
    +167                ps.setInt(1, studentId);
    +168                ps.setInt(2, progressTypeId);
    +169                ps.setString(3, date.toString());
    +170                ps.executeUpdate();
    +171                try (ResultSet keys = ps.getGeneratedKeys()) {
    +172                    if (keys.next()) { return keys.getInt(1); }
    +173                }
    +174            }
    +175        }
    +176        throw new SQLException("Failed to create ProgressSession");
    +177    }
    +178
    +179    /**
    +180     * Insert assessment results for a session. The {@code codes} and {@code scores}
    +181     * arrays must be parallel and correspond to existing AssessmentPart codes.
    +182     * Unknown part codes are ignored.
    +183     *
    +184     * @param sessionId progress session id
    +185     * @param progressTypeId progress type id
    +186     * @param codes array of part codes
    +187     * @param scores array of integer scores
    +188     * @throws SQLException on database errors
    +189     */
    +190    public static void insertAssessmentResults(final int sessionId, final int progressTypeId, final String[] codes, final int[] scores) throws SQLException {
    +191        if (codes.length != scores.length) { throw new IllegalArgumentException("codes and scores length mismatch"); }
    +192        try (Connection c = getConnection()) {
    +193            // cache part ids
    +194            Map<String, Integer> partIdMap = new HashMap<>();
    +195            try (PreparedStatement ps = c.prepareStatement("SELECT id, code FROM AssessmentPart WHERE progress_type_id = ?")) {
    +196                ps.setInt(1, progressTypeId);
    +197                try (ResultSet rs = ps.executeQuery()) {
    +198                    while (rs.next()) {
    +199                        partIdMap.put(rs.getString("code"), rs.getInt("id"));
    +200                    }
    +201                }
    +202            }
    +203            try (PreparedStatement ins = c.prepareStatement("INSERT INTO AssessmentResult(session_id, part_id, score) VALUES (?, ?, ?)") ) {
    +204                for (int i = 0; i < codes.length; i++) {
    +205                    Integer partId = partIdMap.get(codes[i]);
    +206                    if (partId == null) {
    +207                        // skip unknown part
    +208                        continue;
    +209                    }
    +210                    ins.setInt(1, sessionId);
    +211                    ins.setInt(2, partId);
    +212                    ins.setInt(3, scores[i]);
    +213                    ins.addBatch();
    +214                }
    +215                ins.executeBatch();
    +216            }
    +217        }
    +218    }
    +219
    +220    /**
    +221     * Fetch the latest assessment result rows for a named student and progress type.
    +222     * Each returned row is a list of integer scores for the parts in canonical
    +223     * part order.
    +224     *
    +225     * @param studentName student display name
    +226     * @param progressTypeName progress type display name
    +227     * @param limit maximum number of recent sessions to fetch
    +228     * @return list of rows, each row is a list of integer scores
    +229     * @throws SQLException on database errors
    +230     */
    +231    public static List<List<Integer>> fetchLatestAssessmentResults(final String studentName, final String progressTypeName, final int limit) throws SQLException {
    +232        List<List<Integer>> result = new ArrayList<>();
    +233        try (Connection c = getConnection()) {
    +234            Integer studentId = null;
    +235            try (PreparedStatement ps = c.prepareStatement("SELECT id FROM Student WHERE name = ?")) {
    +236                ps.setString(1, studentName);
    +237                try (ResultSet rs = ps.executeQuery()) {
    +238                    if (rs.next()) { studentId = rs.getInt(1); }
    +239                }
    +240            }
    +241            if (studentId == null) { return result; }
    +242
    +243            Integer progressTypeId = null;
    +244            try (PreparedStatement ps = c.prepareStatement("SELECT id FROM ProgressType WHERE name = ?")) {
    +245                ps.setString(1, progressTypeName);
    +246                try (ResultSet rs = ps.executeQuery()) {
    +247                    if (rs.next()) { progressTypeId = rs.getInt(1); }
    +248                }
    +249            }
    +250            if (progressTypeId == null) { return result; }
    +251
    +252            // get parts in canonical order (by id)
    +253            List<Integer> partIds = new ArrayList<>();
    +254            try (PreparedStatement ps = c.prepareStatement("SELECT id, code FROM AssessmentPart WHERE progress_type_id = ? ORDER BY id ASC")) {
    +255                ps.setInt(1, progressTypeId);
    +256                try (ResultSet rs = ps.executeQuery()) {
    +257                    while (rs.next()) { partIds.add(rs.getInt("id")); }
    +258                }
    +259            }
    +260
    +261            // get latest session ids for this student and progress type
    +262            List<Integer> sessionIds = new ArrayList<>();
    +263            try (PreparedStatement ps = c.prepareStatement("SELECT id FROM ProgressSession WHERE student_id = ? AND progress_type_id = ? ORDER BY id DESC LIMIT ?")) {
    +264                ps.setInt(1, studentId);
    +265                ps.setInt(2, progressTypeId);
    +266                ps.setInt(3, limit);
    +267                try (ResultSet rs = ps.executeQuery()) {
    +268                    while (rs.next()) { sessionIds.add(rs.getInt(1)); }
    +269                }
    +270            }
    +271
    +272            // For each session, fetch scores mapped to parts
    +273            for (Integer sid : sessionIds) {
    +274                Map<Integer, Integer> scoreByPart = new HashMap<>();
    +275                try (PreparedStatement ps = c.prepareStatement("SELECT part_id, score FROM AssessmentResult WHERE session_id = ?")) {
    +276                    ps.setInt(1, sid);
    +277                    try (ResultSet rs = ps.executeQuery()) {
    +278                        while (rs.next()) {
    +279                            scoreByPart.put(rs.getInt("part_id"), rs.getInt("score"));
    +280                        }
    +281                    }
    +282                }
    +283                List<Integer> row = new ArrayList<>();
    +284                for (Integer pid : partIds) {
    +285                    Integer s = scoreByPart.get(pid);
    +286                    row.add(s == null ? 0 : s);
    +287                }
    +288                result.add(row);
    +289            }
    +290        }
    +291        return result;
    +292    }
    +293
    +294    /**
    +295     * Simple, immutable holder for time-series assessment results.
    +296     *
    +297     * <p>Contains a chronologically ordered list of session {@code dates}
    +298     * and a parallel list of integer score rows. Each entry in {@code rows}
    +299     * corresponds to the parts for a progress type in canonical order.
    +300     */
    +301    public static class ResultsWithDates {
    +302        /**
    +303         * Ordered session dates (oldest first). Can be empty when no sessions exist.
    +304         */
    +305        public final java.util.List<java.time.LocalDate> dates;
    +306
    +307        /**
    +308         * Parallel rows of integer scores. Each inner list corresponds to the
    +309         * assessment parts for a single session in canonical part order. May be
    +310         * empty when there are no sessions.
    +311         */
    +312        public final java.util.List<java.util.List<Integer>> rows;
    +313
    +314        /**
    +315         * Create a ResultsWithDates instance.
    +316         *
    +317         * @param dates ordered session dates (oldest-first)
    +318         * @param rows parallel list of score rows matching {@code dates}
    +319         */
    +320        public ResultsWithDates(java.util.List<java.time.LocalDate> dates, java.util.List<java.util.List<Integer>> rows) {
    +321            this.dates = dates;
    +322            this.rows = rows;
    +323        }
    +324    }
    +325
    +326    /**
    +327     * Fetch the latest assessment rows along with their session dates.
    +328     * Rows and dates are ordered oldest-first to facilitate time series plotting.
    +329     *
    +330     * @param studentName student display name to filter results for
    +331     * @param progressTypeName progress type display name (e.g., "Braille")
    +332     * @param limit maximum number of recent sessions to return
    +333     * @return ResultsWithDates holding an ordered list of session dates and parallel rows of scores
    +334     * @throws SQLException on database errors
    +335     */
    +336    public static ResultsWithDates fetchLatestAssessmentResultsWithDates(final String studentName, final String progressTypeName, final int limit) throws SQLException {
    +337        java.util.List<java.util.List<Integer>> result = new ArrayList<>();
    +338        java.util.List<java.time.LocalDate> dates = new ArrayList<>();
    +339        try (Connection c = getConnection()) {
    +340            Integer studentId = null;
    +341            try (PreparedStatement ps = c.prepareStatement("SELECT id FROM Student WHERE name = ?")) {
    +342                ps.setString(1, studentName);
    +343                try (ResultSet rs = ps.executeQuery()) {
    +344                    if (rs.next()) {
    +345                        studentId = rs.getInt(1);
    +346                    }
    +347                }
    +348            }
    +349            if (studentId == null) { return new ResultsWithDates(dates, result); }
    +350
    +351            Integer progressTypeId = null;
    +352            try (PreparedStatement ps = c.prepareStatement("SELECT id FROM ProgressType WHERE name = ?")) {
    +353                ps.setString(1, progressTypeName);
    +354                try (ResultSet rs = ps.executeQuery()) {
    +355                    if (rs.next()) {
    +356                        progressTypeId = rs.getInt(1);
    +357                    }
    +358                }
    +359            }
    +360            if (progressTypeId == null) {
    +361                return new ResultsWithDates(dates, result);
    +362            }
    +363
    +364            // get parts in canonical order (by id)
    +365            java.util.List<Integer> partIds = new ArrayList<>();
    +366            try (PreparedStatement ps = c.prepareStatement("SELECT id, code FROM AssessmentPart WHERE progress_type_id = ? ORDER BY id ASC")) {
    +367                ps.setInt(1, progressTypeId);
    +368                try (ResultSet rs = ps.executeQuery()) {
    +369                    while (rs.next()) {
    +370                        partIds.add(rs.getInt("id"));
    +371                    }
    +372                }
    +373            }
    +374
    +375            // get latest session ids and dates for this student and progress type
    +376            java.util.List<java.lang.Integer> sessionIds = new ArrayList<>();
    +377            java.util.List<java.time.LocalDate> sessionDates = new ArrayList<>();
    +378            try (PreparedStatement ps = c.prepareStatement("SELECT id, date FROM ProgressSession WHERE student_id = ? AND progress_type_id = ? ORDER BY id DESC LIMIT ?")) {
    +379                ps.setInt(1, studentId); ps.setInt(2, progressTypeId); ps.setInt(3, limit);
    +380                try (ResultSet rs = ps.executeQuery()) {
    +381                    while (rs.next()) {
    +382                        sessionIds.add(rs.getInt("id"));
    +383                        sessionDates.add(java.time.LocalDate.parse(rs.getString("date")));
    +384                    }
    +385                }
    +386            }
    +387
    +388            // We want chronological order (oldest first)
    +389            java.util.Collections.reverse(sessionIds);
    +390            java.util.Collections.reverse(sessionDates);
    +391
    +392            // For each session, fetch scores mapped to parts and append row
    +393            for (Integer sid : sessionIds) {
    +394                Map<Integer, Integer> scoreByPart = new HashMap<>();
    +395                try (PreparedStatement ps = c.prepareStatement("SELECT part_id, score FROM AssessmentResult WHERE session_id = ?")) {
    +396                    ps.setInt(1, sid);
    +397                    try (ResultSet rs = ps.executeQuery()) {
    +398                        while (rs.next()) {
    +399                            scoreByPart.put(rs.getInt("part_id"), rs.getInt("score"));
    +400                        }
    +401                    }
    +402                }
    +403                java.util.List<Integer> row = new ArrayList<>();
    +404                for (Integer pid : partIds) {
    +405                    Integer s = scoreByPart.get(pid);
    +406                    row.add(s == null ? 0 : s);
    +407                }
    +408                result.add(row);
    +409            }
    +410            dates.addAll(sessionDates);
    +411        }
    +412        return new ResultsWithDates(dates, result);
    +413    }
    +414
    +415    /**
    +416     * Insert a keyboarding-specific result linked to a ProgressSession.
    +417     *
    +418     * @param sessionId existing session id
    +419     * @param program program or curriculum name
    +420     * @param topic topic or lesson name
    +421     * @param speed words-per-minute
    +422     * @param accuracy accuracy percent
    +423     * @throws SQLException on database errors
    +424     */
    +425    public static void insertKeyboardingResult(final int sessionId, final String program, final String topic, final int speed, final int accuracy) throws SQLException {
    +426        try (Connection c = getConnection()) {
    +427            try (PreparedStatement ps = c.prepareStatement("INSERT INTO KeyboardingResult(session_id, program, topic, speed, accuracy) VALUES (?, ?, ?, ?, ?)")) {
    +428                ps.setInt(1, sessionId);
    +429                ps.setString(2, program);
    +430                ps.setString(3, topic);
    +431                ps.setInt(4, speed);
    +432                ps.setInt(5, accuracy);
    +433                ps.executeUpdate();
    +434            }
    +435        }
    +436    }
    +437
    +438    /**
    +439     * Save free-form notes for a given ProgressSession.
    +440     *
    +441     * @param sessionId progress session id
    +442     * @param notes free-form notes text
    +443     * @throws SQLException on database errors
    +444     */
    +445    public static void saveSessionNotes(final int sessionId, final String notes) throws SQLException {
    +446        try (Connection c = getConnection()) {
    +447            try (PreparedStatement ps = c.prepareStatement("UPDATE ProgressSession SET notes = ? WHERE id = ?")) {
    +448                ps.setString(1, notes);
    +449                ps.setInt(2, sessionId);
    +450                ps.executeUpdate();
    +451            }
    +452        }
    +453    }
    +454
    +455    /**
    +456     * Save structured contact log details for a given ProgressSession. This
    +457     * will insert or replace a single ContactLog row tied to the session.
    +458     *
    +459     * @param sessionId existing session id
    +460     * @param studentName student display name
    +461     * @param date session date as text
    +462     * @param guardianName guardian or parent name
    +463     * @param contactMethod method of contact (phone/email/etc)
    +464     * @param phoneNumber phone number string
    +465     * @param emailAddress email address string
    +466     * @param contactResponse short description of response
    +467     * @param contactGeneral general contact summary
    +468     * @param contactSpecific specific items discussed
    +469     * @param contactNotes free-form notes
    +470     * @throws SQLException on database errors
    +471     */
    +472    public static void saveContactLog(final int sessionId, final String studentName, final String date, final String guardianName, final String contactMethod, final String phoneNumber, final String emailAddress, final String contactResponse, final String contactGeneral, final String contactSpecific, final String contactNotes) throws SQLException {
    +473        try (Connection c = getConnection()) {
    +474            try (PreparedStatement ps = c.prepareStatement("INSERT OR REPLACE INTO ContactLog(session_id, student_name, date, guardian_name, contact_method, phone_number, email_address, contact_response, contact_general, contact_specific, contact_notes) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)") ) {
    +475                ps.setInt(1, sessionId);
    +476                ps.setString(2, studentName);
    +477                ps.setString(3, date);
    +478                ps.setString(4, guardianName);
    +479                ps.setString(5, contactMethod);
    +480                ps.setString(6, phoneNumber);
    +481                ps.setString(7, emailAddress);
    +482                ps.setString(8, contactResponse);
    +483                ps.setString(9, contactGeneral);
    +484                ps.setString(10, contactSpecific);
    +485                ps.setString(11, contactNotes);
    +486                ps.executeUpdate();
    +487            }
    +488        }
    +489    }
    +490
    +491    /**
    +492     * Fetch the most recent ContactLog entry for the given student name.
    +493     * Returns a map of column names to string values, or null if none found.
    +494    *
    +495    * @param studentName student display name to search for
    +496    * @return map of contact log columns to values or null when not found
    +497    * @throws SQLException on database errors
    +498     */
    +499    public static com.studentgui.apphelpers.dto.ContactPayload fetchLatestContactLog(final String studentName) throws SQLException {
    +500        try (Connection c = getConnection()) {
    +501            Integer studentId = null;
    +502            try (PreparedStatement ps = c.prepareStatement("SELECT id FROM Student WHERE name = ?")) {
    +503                ps.setString(1, studentName);
    +504                try (ResultSet rs = ps.executeQuery()) {
    +505                    if (rs.next()) {
    +506                        studentId = rs.getInt(1);
    +507                    }
    +508                }
    +509            }
    +510            if (studentId == null) {
    +511                return null;
    +512            }
    +513
    +514            // Find the latest session id for ProgressType 'ContactLog'
    +515            Integer ptId = null;
    +516            try (PreparedStatement ps = c.prepareStatement("SELECT id FROM ProgressType WHERE name = ?")) {
    +517                ps.setString(1, "ContactLog");
    +518                try (ResultSet rs = ps.executeQuery()) {
    +519                    if (rs.next()) {
    +520                        ptId = rs.getInt(1);
    +521                    }
    +522                }
    +523            }
    +524            if (ptId == null) {
    +525                return null;
    +526            }
    +527
    +528            Integer sessionId = null;
    +529            try (PreparedStatement ps = c.prepareStatement("SELECT id FROM ProgressSession WHERE student_id = ? AND progress_type_id = ? ORDER BY id DESC LIMIT 1")) {
    +530                ps.setInt(1, studentId);
    +531                ps.setInt(2, ptId);
    +532                try (ResultSet rs = ps.executeQuery()) {
    +533                    if (rs.next()) {
    +534                        sessionId = rs.getInt(1);
    +535                    }
    +536                }
    +537            }
    +538            if (sessionId == null) {
    +539                return null;
    +540            }
    +541
    +542            try (PreparedStatement ps = c.prepareStatement("SELECT student_name, date, guardian_name, contact_method, phone_number, email_address, contact_response, contact_general, contact_specific, contact_notes FROM ContactLog WHERE session_id = ? ORDER BY id DESC LIMIT 1")) {
    +543                ps.setInt(1, sessionId);
    +544                try (ResultSet rs = ps.executeQuery()) {
    +545                    if (rs.next()) {
    +546                        com.studentgui.apphelpers.dto.ContactPayload p = new com.studentgui.apphelpers.dto.ContactPayload(
    +547                            sessionId,
    +548                            rs.getString("guardian_name"),
    +549                            rs.getString("contact_method"),
    +550                            rs.getString("phone_number"),
    +551                            rs.getString("email_address"),
    +552                            rs.getString("contact_response"),
    +553                            rs.getString("contact_general"),
    +554                            rs.getString("contact_specific"),
    +555                            rs.getString("contact_notes")
    +556                        );
    +557                        return p;
    +558                    }
    +559                }
    +560            }
    +561            return null;
    +562        }
    +563    }
    +564
    +565}
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    + + diff --git a/target/site/apidocs/src-html/com/studentgui/apphelpers/Database.html b/target/site/apidocs/src-html/com/studentgui/apphelpers/Database.html new file mode 100644 index 0000000..e2e2cc7 --- /dev/null +++ b/target/site/apidocs/src-html/com/studentgui/apphelpers/Database.html @@ -0,0 +1,643 @@ + + + + +Source code + + + + + + +
    +
    +
    001package com.studentgui.apphelpers;
    +002
    +003import java.sql.Connection;
    +004import java.sql.DriverManager;
    +005import java.sql.PreparedStatement;
    +006import java.sql.ResultSet;
    +007import java.sql.SQLException;
    +008import java.sql.Statement;
    +009import java.time.LocalDate;
    +010import java.util.ArrayList;
    +011import java.util.HashMap;
    +012import java.util.List;
    +013import java.util.Map;
    +014
    +015/**
    +016 * Centralized database helper for the normalized SQLite schema.
    +017 *
    +018 * <p>Provides convenience methods to get-or-create Students and ProgressTypes,
    +019 * create ProgressSessions, ensure AssessmentParts, insert/fetch assessment
    +020 * results, and save session-specific notes. Use these helpers instead of
    +021 * running per-page DDL throughout the codebase.</p>
    +022 */
    +023public class Database {
    +024
    +025    /**
    +026     * Private constructor to prevent instantiation of this utility class.
    +027     */
    +028    private Database() {
    +029        throw new AssertionError("Database is a utility class");
    +030    }
    +031
    +032    /**
    +033     * Obtain a new JDBC Connection to the application SQLite database.
    +034     * Caller is responsible for closing the connection (try-with-resources is recommended).
    +035     *
    +036     * @return new Connection
    +037     * @throws SQLException if the driver cannot open the database
    +038     */
    +039    private static Connection getConnection() throws SQLException {
    +040        String url = "jdbc:sqlite:" + Helpers.DATABASE_PATH.toString();
    +041        return DriverManager.getConnection(url);
    +042    }
    +043    
    +044    /**
    +045     * Get a student id by name, creating a new Student row when none exists.
    +046     *
    +047     * @param name student display name
    +048     * @return id of the existing or newly created student
    +049     * @throws SQLException on database errors
    +050     */
    +051    public static int getOrCreateStudent(final String name) throws SQLException {
    +052        try (Connection c = getConnection()) {
    +053            try (PreparedStatement ps = c.prepareStatement("SELECT id FROM Student WHERE name = ?")) {
    +054                ps.setString(1, name);
    +055                try (ResultSet rs = ps.executeQuery()) {
    +056                    if (rs.next()) {
    +057                        return rs.getInt(1);
    +058                    }
    +059                }
    +060            }
    +061            try (PreparedStatement ps = c.prepareStatement("INSERT INTO Student(name) VALUES (?)", Statement.RETURN_GENERATED_KEYS)) {
    +062                ps.setString(1, name);
    +063                ps.executeUpdate();
    +064                try (ResultSet keys = ps.getGeneratedKeys()) {
    +065                    if (keys.next()) {
    +066                        return keys.getInt(1);
    +067                    }
    +068                }
    +069            }
    +070        }
    +071        throw new SQLException("Failed to create or retrieve student");
    +072    }
    +073
    +074    /**
    +075     * Get or create a ProgressType row by name.
    +076     *
    +077     * @param name progress type display name
    +078     * @return database id of the progress type
    +079     * @throws SQLException on database errors
    +080     */
    +081    public static int getOrCreateProgressType(final String name) throws SQLException {
    +082        try (Connection c = getConnection()) {
    +083            try (PreparedStatement ps = c.prepareStatement("SELECT id FROM ProgressType WHERE name = ?")) {
    +084                ps.setString(1, name);
    +085                try (ResultSet rs = ps.executeQuery()) {
    +086                    if (rs.next()) {
    +087                        return rs.getInt(1);
    +088                    }
    +089                }
    +090            }
    +091            try (PreparedStatement ps = c.prepareStatement("INSERT INTO ProgressType(name) VALUES (?)", Statement.RETURN_GENERATED_KEYS)) {
    +092                ps.setString(1, name);
    +093                ps.executeUpdate();
    +094                try (ResultSet keys = ps.getGeneratedKeys()) {
    +095                    if (keys.next()) {
    +096                        return keys.getInt(1);
    +097                    }
    +098                }
    +099            }
    +100        }
    +101        throw new SQLException("Failed to create or retrieve ProgressType");
    +102    }
    +103
    +104    /**
    +105     * Ensure AssessmentPart rows exist for the given progress type. This uses
    +106     * SQL "INSERT OR IGNORE" so existing parts are preserved.
    +107     *
    +108     * @param progressTypeId id of the ProgressType
    +109     * @param codes array of part codes to ensure
    +110     * @throws SQLException on database errors
    +111     */
    +112    public static void ensureAssessmentParts(final int progressTypeId, final String[] codes) throws SQLException {
    +113        try (Connection c = getConnection()) {
    +114            try (PreparedStatement ps = c.prepareStatement("INSERT OR IGNORE INTO AssessmentPart(progress_type_id, code, description) VALUES (?, ?, NULL)")) {
    +115                for (String code : codes) {
    +116                    ps.setInt(1, progressTypeId);
    +117                    ps.setString(2, code);
    +118                    ps.addBatch();
    +119                }
    +120                ps.executeBatch();
    +121            }
    +122        }
    +123    }
    +124
    +125    /**
    +126     * Remove any AssessmentPart rows for the given progress type whose code is
    +127     * not present in the provided canonical codes array. This helps clean up
    +128     * legacy/malformed entries that could cause part ordering mismatches.
    +129     *
    +130     * @param progressTypeId id of the ProgressType
    +131     * @param allowedCodes canonical set of codes to keep
    +132     * @throws SQLException on database errors
    +133     */
    +134    public static void cleanupAssessmentParts(final int progressTypeId, final String[] allowedCodes) throws SQLException {
    +135        if (allowedCodes == null || allowedCodes.length == 0) {
    +136            return;
    +137        }
    +138        try (Connection c = getConnection()) {
    +139            StringBuilder sb = new StringBuilder();
    +140            for (int i = 0; i < allowedCodes.length; i++) {
    +141                if (i > 0) { sb.append(','); }
    +142                sb.append('?');
    +143            }
    +144            String sql = "DELETE FROM AssessmentPart WHERE progress_type_id = ? AND code NOT IN (" + sb.toString() + ")";
    +145            try (PreparedStatement ps = c.prepareStatement(sql)) {
    +146                ps.setInt(1, progressTypeId);
    +147                for (int i = 0; i < allowedCodes.length; i++) {
    +148                    ps.setString(i + 2, allowedCodes[i]);
    +149                }
    +150                ps.executeUpdate();
    +151            }
    +152        }
    +153    }
    +154
    +155    /**
    +156     * Create a ProgressSession for a student and progress type on the given date.
    +157     *
    +158     * @param studentId existing student id
    +159     * @param progressTypeId existing progress type id
    +160     * @param date session date
    +161     * @return generated ProgressSession id
    +162     * @throws SQLException on database errors
    +163     */
    +164    public static int createProgressSession(final int studentId, final int progressTypeId, final LocalDate date) throws SQLException {
    +165        try (Connection c = getConnection()) {
    +166            try (PreparedStatement ps = c.prepareStatement("INSERT INTO ProgressSession(student_id, progress_type_id, date, notes) VALUES (?, ?, ?, NULL)", Statement.RETURN_GENERATED_KEYS)) {
    +167                ps.setInt(1, studentId);
    +168                ps.setInt(2, progressTypeId);
    +169                ps.setString(3, date.toString());
    +170                ps.executeUpdate();
    +171                try (ResultSet keys = ps.getGeneratedKeys()) {
    +172                    if (keys.next()) { return keys.getInt(1); }
    +173                }
    +174            }
    +175        }
    +176        throw new SQLException("Failed to create ProgressSession");
    +177    }
    +178
    +179    /**
    +180     * Insert assessment results for a session. The {@code codes} and {@code scores}
    +181     * arrays must be parallel and correspond to existing AssessmentPart codes.
    +182     * Unknown part codes are ignored.
    +183     *
    +184     * @param sessionId progress session id
    +185     * @param progressTypeId progress type id
    +186     * @param codes array of part codes
    +187     * @param scores array of integer scores
    +188     * @throws SQLException on database errors
    +189     */
    +190    public static void insertAssessmentResults(final int sessionId, final int progressTypeId, final String[] codes, final int[] scores) throws SQLException {
    +191        if (codes.length != scores.length) { throw new IllegalArgumentException("codes and scores length mismatch"); }
    +192        try (Connection c = getConnection()) {
    +193            // cache part ids
    +194            Map<String, Integer> partIdMap = new HashMap<>();
    +195            try (PreparedStatement ps = c.prepareStatement("SELECT id, code FROM AssessmentPart WHERE progress_type_id = ?")) {
    +196                ps.setInt(1, progressTypeId);
    +197                try (ResultSet rs = ps.executeQuery()) {
    +198                    while (rs.next()) {
    +199                        partIdMap.put(rs.getString("code"), rs.getInt("id"));
    +200                    }
    +201                }
    +202            }
    +203            try (PreparedStatement ins = c.prepareStatement("INSERT INTO AssessmentResult(session_id, part_id, score) VALUES (?, ?, ?)") ) {
    +204                for (int i = 0; i < codes.length; i++) {
    +205                    Integer partId = partIdMap.get(codes[i]);
    +206                    if (partId == null) {
    +207                        // skip unknown part
    +208                        continue;
    +209                    }
    +210                    ins.setInt(1, sessionId);
    +211                    ins.setInt(2, partId);
    +212                    ins.setInt(3, scores[i]);
    +213                    ins.addBatch();
    +214                }
    +215                ins.executeBatch();
    +216            }
    +217        }
    +218    }
    +219
    +220    /**
    +221     * Fetch the latest assessment result rows for a named student and progress type.
    +222     * Each returned row is a list of integer scores for the parts in canonical
    +223     * part order.
    +224     *
    +225     * @param studentName student display name
    +226     * @param progressTypeName progress type display name
    +227     * @param limit maximum number of recent sessions to fetch
    +228     * @return list of rows, each row is a list of integer scores
    +229     * @throws SQLException on database errors
    +230     */
    +231    public static List<List<Integer>> fetchLatestAssessmentResults(final String studentName, final String progressTypeName, final int limit) throws SQLException {
    +232        List<List<Integer>> result = new ArrayList<>();
    +233        try (Connection c = getConnection()) {
    +234            Integer studentId = null;
    +235            try (PreparedStatement ps = c.prepareStatement("SELECT id FROM Student WHERE name = ?")) {
    +236                ps.setString(1, studentName);
    +237                try (ResultSet rs = ps.executeQuery()) {
    +238                    if (rs.next()) { studentId = rs.getInt(1); }
    +239                }
    +240            }
    +241            if (studentId == null) { return result; }
    +242
    +243            Integer progressTypeId = null;
    +244            try (PreparedStatement ps = c.prepareStatement("SELECT id FROM ProgressType WHERE name = ?")) {
    +245                ps.setString(1, progressTypeName);
    +246                try (ResultSet rs = ps.executeQuery()) {
    +247                    if (rs.next()) { progressTypeId = rs.getInt(1); }
    +248                }
    +249            }
    +250            if (progressTypeId == null) { return result; }
    +251
    +252            // get parts in canonical order (by id)
    +253            List<Integer> partIds = new ArrayList<>();
    +254            try (PreparedStatement ps = c.prepareStatement("SELECT id, code FROM AssessmentPart WHERE progress_type_id = ? ORDER BY id ASC")) {
    +255                ps.setInt(1, progressTypeId);
    +256                try (ResultSet rs = ps.executeQuery()) {
    +257                    while (rs.next()) { partIds.add(rs.getInt("id")); }
    +258                }
    +259            }
    +260
    +261            // get latest session ids for this student and progress type
    +262            List<Integer> sessionIds = new ArrayList<>();
    +263            try (PreparedStatement ps = c.prepareStatement("SELECT id FROM ProgressSession WHERE student_id = ? AND progress_type_id = ? ORDER BY id DESC LIMIT ?")) {
    +264                ps.setInt(1, studentId);
    +265                ps.setInt(2, progressTypeId);
    +266                ps.setInt(3, limit);
    +267                try (ResultSet rs = ps.executeQuery()) {
    +268                    while (rs.next()) { sessionIds.add(rs.getInt(1)); }
    +269                }
    +270            }
    +271
    +272            // For each session, fetch scores mapped to parts
    +273            for (Integer sid : sessionIds) {
    +274                Map<Integer, Integer> scoreByPart = new HashMap<>();
    +275                try (PreparedStatement ps = c.prepareStatement("SELECT part_id, score FROM AssessmentResult WHERE session_id = ?")) {
    +276                    ps.setInt(1, sid);
    +277                    try (ResultSet rs = ps.executeQuery()) {
    +278                        while (rs.next()) {
    +279                            scoreByPart.put(rs.getInt("part_id"), rs.getInt("score"));
    +280                        }
    +281                    }
    +282                }
    +283                List<Integer> row = new ArrayList<>();
    +284                for (Integer pid : partIds) {
    +285                    Integer s = scoreByPart.get(pid);
    +286                    row.add(s == null ? 0 : s);
    +287                }
    +288                result.add(row);
    +289            }
    +290        }
    +291        return result;
    +292    }
    +293
    +294    /**
    +295     * Simple, immutable holder for time-series assessment results.
    +296     *
    +297     * <p>Contains a chronologically ordered list of session {@code dates}
    +298     * and a parallel list of integer score rows. Each entry in {@code rows}
    +299     * corresponds to the parts for a progress type in canonical order.
    +300     */
    +301    public static class ResultsWithDates {
    +302        /**
    +303         * Ordered session dates (oldest first). Can be empty when no sessions exist.
    +304         */
    +305        public final java.util.List<java.time.LocalDate> dates;
    +306
    +307        /**
    +308         * Parallel rows of integer scores. Each inner list corresponds to the
    +309         * assessment parts for a single session in canonical part order. May be
    +310         * empty when there are no sessions.
    +311         */
    +312        public final java.util.List<java.util.List<Integer>> rows;
    +313
    +314        /**
    +315         * Create a ResultsWithDates instance.
    +316         *
    +317         * @param dates ordered session dates (oldest-first)
    +318         * @param rows parallel list of score rows matching {@code dates}
    +319         */
    +320        public ResultsWithDates(java.util.List<java.time.LocalDate> dates, java.util.List<java.util.List<Integer>> rows) {
    +321            this.dates = dates;
    +322            this.rows = rows;
    +323        }
    +324    }
    +325
    +326    /**
    +327     * Fetch the latest assessment rows along with their session dates.
    +328     * Rows and dates are ordered oldest-first to facilitate time series plotting.
    +329     *
    +330     * @param studentName student display name to filter results for
    +331     * @param progressTypeName progress type display name (e.g., "Braille")
    +332     * @param limit maximum number of recent sessions to return
    +333     * @return ResultsWithDates holding an ordered list of session dates and parallel rows of scores
    +334     * @throws SQLException on database errors
    +335     */
    +336    public static ResultsWithDates fetchLatestAssessmentResultsWithDates(final String studentName, final String progressTypeName, final int limit) throws SQLException {
    +337        java.util.List<java.util.List<Integer>> result = new ArrayList<>();
    +338        java.util.List<java.time.LocalDate> dates = new ArrayList<>();
    +339        try (Connection c = getConnection()) {
    +340            Integer studentId = null;
    +341            try (PreparedStatement ps = c.prepareStatement("SELECT id FROM Student WHERE name = ?")) {
    +342                ps.setString(1, studentName);
    +343                try (ResultSet rs = ps.executeQuery()) {
    +344                    if (rs.next()) {
    +345                        studentId = rs.getInt(1);
    +346                    }
    +347                }
    +348            }
    +349            if (studentId == null) { return new ResultsWithDates(dates, result); }
    +350
    +351            Integer progressTypeId = null;
    +352            try (PreparedStatement ps = c.prepareStatement("SELECT id FROM ProgressType WHERE name = ?")) {
    +353                ps.setString(1, progressTypeName);
    +354                try (ResultSet rs = ps.executeQuery()) {
    +355                    if (rs.next()) {
    +356                        progressTypeId = rs.getInt(1);
    +357                    }
    +358                }
    +359            }
    +360            if (progressTypeId == null) {
    +361                return new ResultsWithDates(dates, result);
    +362            }
    +363
    +364            // get parts in canonical order (by id)
    +365            java.util.List<Integer> partIds = new ArrayList<>();
    +366            try (PreparedStatement ps = c.prepareStatement("SELECT id, code FROM AssessmentPart WHERE progress_type_id = ? ORDER BY id ASC")) {
    +367                ps.setInt(1, progressTypeId);
    +368                try (ResultSet rs = ps.executeQuery()) {
    +369                    while (rs.next()) {
    +370                        partIds.add(rs.getInt("id"));
    +371                    }
    +372                }
    +373            }
    +374
    +375            // get latest session ids and dates for this student and progress type
    +376            java.util.List<java.lang.Integer> sessionIds = new ArrayList<>();
    +377            java.util.List<java.time.LocalDate> sessionDates = new ArrayList<>();
    +378            try (PreparedStatement ps = c.prepareStatement("SELECT id, date FROM ProgressSession WHERE student_id = ? AND progress_type_id = ? ORDER BY id DESC LIMIT ?")) {
    +379                ps.setInt(1, studentId); ps.setInt(2, progressTypeId); ps.setInt(3, limit);
    +380                try (ResultSet rs = ps.executeQuery()) {
    +381                    while (rs.next()) {
    +382                        sessionIds.add(rs.getInt("id"));
    +383                        sessionDates.add(java.time.LocalDate.parse(rs.getString("date")));
    +384                    }
    +385                }
    +386            }
    +387
    +388            // We want chronological order (oldest first)
    +389            java.util.Collections.reverse(sessionIds);
    +390            java.util.Collections.reverse(sessionDates);
    +391
    +392            // For each session, fetch scores mapped to parts and append row
    +393            for (Integer sid : sessionIds) {
    +394                Map<Integer, Integer> scoreByPart = new HashMap<>();
    +395                try (PreparedStatement ps = c.prepareStatement("SELECT part_id, score FROM AssessmentResult WHERE session_id = ?")) {
    +396                    ps.setInt(1, sid);
    +397                    try (ResultSet rs = ps.executeQuery()) {
    +398                        while (rs.next()) {
    +399                            scoreByPart.put(rs.getInt("part_id"), rs.getInt("score"));
    +400                        }
    +401                    }
    +402                }
    +403                java.util.List<Integer> row = new ArrayList<>();
    +404                for (Integer pid : partIds) {
    +405                    Integer s = scoreByPart.get(pid);
    +406                    row.add(s == null ? 0 : s);
    +407                }
    +408                result.add(row);
    +409            }
    +410            dates.addAll(sessionDates);
    +411        }
    +412        return new ResultsWithDates(dates, result);
    +413    }
    +414
    +415    /**
    +416     * Insert a keyboarding-specific result linked to a ProgressSession.
    +417     *
    +418     * @param sessionId existing session id
    +419     * @param program program or curriculum name
    +420     * @param topic topic or lesson name
    +421     * @param speed words-per-minute
    +422     * @param accuracy accuracy percent
    +423     * @throws SQLException on database errors
    +424     */
    +425    public static void insertKeyboardingResult(final int sessionId, final String program, final String topic, final int speed, final int accuracy) throws SQLException {
    +426        try (Connection c = getConnection()) {
    +427            try (PreparedStatement ps = c.prepareStatement("INSERT INTO KeyboardingResult(session_id, program, topic, speed, accuracy) VALUES (?, ?, ?, ?, ?)")) {
    +428                ps.setInt(1, sessionId);
    +429                ps.setString(2, program);
    +430                ps.setString(3, topic);
    +431                ps.setInt(4, speed);
    +432                ps.setInt(5, accuracy);
    +433                ps.executeUpdate();
    +434            }
    +435        }
    +436    }
    +437
    +438    /**
    +439     * Save free-form notes for a given ProgressSession.
    +440     *
    +441     * @param sessionId progress session id
    +442     * @param notes free-form notes text
    +443     * @throws SQLException on database errors
    +444     */
    +445    public static void saveSessionNotes(final int sessionId, final String notes) throws SQLException {
    +446        try (Connection c = getConnection()) {
    +447            try (PreparedStatement ps = c.prepareStatement("UPDATE ProgressSession SET notes = ? WHERE id = ?")) {
    +448                ps.setString(1, notes);
    +449                ps.setInt(2, sessionId);
    +450                ps.executeUpdate();
    +451            }
    +452        }
    +453    }
    +454
    +455    /**
    +456     * Save structured contact log details for a given ProgressSession. This
    +457     * will insert or replace a single ContactLog row tied to the session.
    +458     *
    +459     * @param sessionId existing session id
    +460     * @param studentName student display name
    +461     * @param date session date as text
    +462     * @param guardianName guardian or parent name
    +463     * @param contactMethod method of contact (phone/email/etc)
    +464     * @param phoneNumber phone number string
    +465     * @param emailAddress email address string
    +466     * @param contactResponse short description of response
    +467     * @param contactGeneral general contact summary
    +468     * @param contactSpecific specific items discussed
    +469     * @param contactNotes free-form notes
    +470     * @throws SQLException on database errors
    +471     */
    +472    public static void saveContactLog(final int sessionId, final String studentName, final String date, final String guardianName, final String contactMethod, final String phoneNumber, final String emailAddress, final String contactResponse, final String contactGeneral, final String contactSpecific, final String contactNotes) throws SQLException {
    +473        try (Connection c = getConnection()) {
    +474            try (PreparedStatement ps = c.prepareStatement("INSERT OR REPLACE INTO ContactLog(session_id, student_name, date, guardian_name, contact_method, phone_number, email_address, contact_response, contact_general, contact_specific, contact_notes) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)") ) {
    +475                ps.setInt(1, sessionId);
    +476                ps.setString(2, studentName);
    +477                ps.setString(3, date);
    +478                ps.setString(4, guardianName);
    +479                ps.setString(5, contactMethod);
    +480                ps.setString(6, phoneNumber);
    +481                ps.setString(7, emailAddress);
    +482                ps.setString(8, contactResponse);
    +483                ps.setString(9, contactGeneral);
    +484                ps.setString(10, contactSpecific);
    +485                ps.setString(11, contactNotes);
    +486                ps.executeUpdate();
    +487            }
    +488        }
    +489    }
    +490
    +491    /**
    +492     * Fetch the most recent ContactLog entry for the given student name.
    +493     * Returns a map of column names to string values, or null if none found.
    +494    *
    +495    * @param studentName student display name to search for
    +496    * @return map of contact log columns to values or null when not found
    +497    * @throws SQLException on database errors
    +498     */
    +499    public static com.studentgui.apphelpers.dto.ContactPayload fetchLatestContactLog(final String studentName) throws SQLException {
    +500        try (Connection c = getConnection()) {
    +501            Integer studentId = null;
    +502            try (PreparedStatement ps = c.prepareStatement("SELECT id FROM Student WHERE name = ?")) {
    +503                ps.setString(1, studentName);
    +504                try (ResultSet rs = ps.executeQuery()) {
    +505                    if (rs.next()) {
    +506                        studentId = rs.getInt(1);
    +507                    }
    +508                }
    +509            }
    +510            if (studentId == null) {
    +511                return null;
    +512            }
    +513
    +514            // Find the latest session id for ProgressType 'ContactLog'
    +515            Integer ptId = null;
    +516            try (PreparedStatement ps = c.prepareStatement("SELECT id FROM ProgressType WHERE name = ?")) {
    +517                ps.setString(1, "ContactLog");
    +518                try (ResultSet rs = ps.executeQuery()) {
    +519                    if (rs.next()) {
    +520                        ptId = rs.getInt(1);
    +521                    }
    +522                }
    +523            }
    +524            if (ptId == null) {
    +525                return null;
    +526            }
    +527
    +528            Integer sessionId = null;
    +529            try (PreparedStatement ps = c.prepareStatement("SELECT id FROM ProgressSession WHERE student_id = ? AND progress_type_id = ? ORDER BY id DESC LIMIT 1")) {
    +530                ps.setInt(1, studentId);
    +531                ps.setInt(2, ptId);
    +532                try (ResultSet rs = ps.executeQuery()) {
    +533                    if (rs.next()) {
    +534                        sessionId = rs.getInt(1);
    +535                    }
    +536                }
    +537            }
    +538            if (sessionId == null) {
    +539                return null;
    +540            }
    +541
    +542            try (PreparedStatement ps = c.prepareStatement("SELECT student_name, date, guardian_name, contact_method, phone_number, email_address, contact_response, contact_general, contact_specific, contact_notes FROM ContactLog WHERE session_id = ? ORDER BY id DESC LIMIT 1")) {
    +543                ps.setInt(1, sessionId);
    +544                try (ResultSet rs = ps.executeQuery()) {
    +545                    if (rs.next()) {
    +546                        com.studentgui.apphelpers.dto.ContactPayload p = new com.studentgui.apphelpers.dto.ContactPayload(
    +547                            sessionId,
    +548                            rs.getString("guardian_name"),
    +549                            rs.getString("contact_method"),
    +550                            rs.getString("phone_number"),
    +551                            rs.getString("email_address"),
    +552                            rs.getString("contact_response"),
    +553                            rs.getString("contact_general"),
    +554                            rs.getString("contact_specific"),
    +555                            rs.getString("contact_notes")
    +556                        );
    +557                        return p;
    +558                    }
    +559                }
    +560            }
    +561            return null;
    +562        }
    +563    }
    +564
    +565}
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    + + diff --git a/target/site/apidocs/src-html/com/studentgui/apphelpers/Helpers.html b/target/site/apidocs/src-html/com/studentgui/apphelpers/Helpers.html new file mode 100644 index 0000000..0d35795 --- /dev/null +++ b/target/site/apidocs/src-html/com/studentgui/apphelpers/Helpers.html @@ -0,0 +1,377 @@ + + + + +Source code + + + + + + +
    +
    +
    001package com.studentgui.apphelpers;
    +002
    +003import java.io.IOException;
    +004import java.nio.file.Files;
    +005import java.nio.file.Path;
    +006import java.nio.file.Paths;
    +007import java.util.ArrayList;
    +008import java.util.List;
    +009
    +010/**
    +011 * Miscellaneous filesystem and small utility helpers used by the UI pages.
    +012 *
    +013 * Responsibilities include selecting and creating the application home
    +014 * directory, creating per-student folder hierarchies, and providing a
    +015 * small roster fallback when no students.json exists.
    +016 */
    +017public class Helpers {
    +018    /**
    +019     * Private constructor to prevent instantiation of this utility class.
    +020     */
    +021    private Helpers() {
    +022        throw new AssertionError("Helpers is a utility class");
    +023    }
    +024    private static final org.slf4j.Logger LOG = org.slf4j.LoggerFactory.getLogger(Helpers.class);
    +025    /** The project working directory (where the process was started). */
    +026    public static final Path PROJECT_ROOT = Paths.get(System.getProperty("user.dir"));
    +027    /** Application home used for storing app-specific files (defaults to ./app_home). */
    +028    public static final Path APP_HOME = selectAppHome();
    +029    /** Root directory for persisted application data (alias of APP_HOME). */
    +030    public static final Path DATA_ROOT = APP_HOME;
    +031    /** Directory that holds the database file. */
    +032    public static final Path DATABASE_ROOT = DATA_ROOT.resolve("StudentDatabase");
    +033    /** Canonical database file path used by SQLite operations. */
    +034    public static final Path DATABASE_PATH = DATABASE_ROOT.resolve("students20252026.db");
    +035
    +036    /**
    +037     * Select a suitable application home directory. Attempts to use a
    +038     * ./app_home subdirectory of the working directory and falls back to the
    +039     * system temporary directory if creation fails.
    +040     */
    +041    private static Path selectAppHome() {
    +042        try {
    +043            Path candidate = PROJECT_ROOT.resolve("app_home");
    +044            Files.createDirectories(candidate);
    +045            // test write
    +046            Path test = candidate.resolve(".write_test");
    +047            Files.writeString(test, "");
    +048            Files.deleteIfExists(test);
    +049            return candidate;
    +050        } catch (IOException e) {
    +051                LOG.debug("Unable to create app_home; falling back to temp dir", e);
    +052                try {
    +053                    Path tmp = Paths.get(System.getProperty("java.io.tmpdir"), "StudentDataGUI");
    +054                    Files.createDirectories(tmp);
    +055                    return tmp;
    +056                } catch (IOException ex) {
    +057                    LOG.debug("Unable to create fallback temp dir; using CWD", ex);
    +058                    return Paths.get(".");
    +059                }
    +060        }
    +061    }
    +062
    +063    /**
    +064     * Attempt to set the JVM working directory to APP_HOME. Fails silently if
    +065     * the property cannot be set in the running environment.
    +066     */
    +067    public static void setStartDir() {
    +068        /**
    +069         * Set the JVM working directory to the application home when possible.
    +070         * Fail silently if the property cannot be set.
    +071         */
    +072        try {
    +073            System.setProperty("user.dir", APP_HOME.toString());
    +074        } catch (SecurityException se) {
    +075            LOG.debug("Unable to set user.dir to APP_HOME {}", APP_HOME, se);
    +076        }
    +077    }
    +078
    +079    /**
    +080     * Ensure the working data directory exists under APP_HOME. This is
    +081     * idempotent and safe to call on startup.
    +082     */
    +083    public static void workingDir() {
    +084        /**
    +085         * Ensure the working data directory exists under the application home.
    +086         */
    +087        try {
    +088            Path studentDataDir = APP_HOME.resolve("StudentDataFiles");
    +089            Files.createDirectories(studentDataDir);
    +090        } catch (IOException ioe) {
    +091            LOG.debug("Unable to create StudentDataFiles directory under {}", APP_HOME, ioe);
    +092        }
    +093    }
    +094
    +095    /**
    +096     * Create a basic folder hierarchy under DATA_ROOT for each student.
    +097     * This will create StudentDataFiles, backups and errorLogs and a
    +098     * per-student folder with subfolders for data sheets and materials.
    +099     */
    +100    public static void createFolderHierarchy() {
    +101        /**
    +102         * Create a basic folder hierarchy under DATA_ROOT for each student.
    +103         * This is idempotent and will create per-student subfolders and an
    +104         * omnibus csv file when missing.
    +105         */
    +106        // Create basic folders for each student in a simple roster
    +107        List<String> students = getStudents();
    +108        Path studentDatafilesRoot = DATA_ROOT.resolve("StudentDataFiles");
    +109        Path studentErrorlogsRoot = DATA_ROOT.resolve("errorLogs");
    +110        Path studentBackupsRoot = DATA_ROOT.resolve("backups");
    +111        try {
    +112            Files.createDirectories(studentDatafilesRoot);
    +113            Files.createDirectories(studentErrorlogsRoot);
    +114            Files.createDirectories(studentBackupsRoot);
    +115        } catch (IOException ioe) {
    +116            LOG.debug("Unable to create one or more data folders under {}", DATA_ROOT, ioe);
    +117        }
    +118
    +119        for (String name : students) {
    +120            String safe = sanitize(name);
    +121            Path studentFolder = studentDatafilesRoot.resolve(safe);
    +122            try {
    +123                Files.createDirectories(studentFolder.resolve("StudentDataSheets"));
    +124                Files.createDirectories(studentFolder.resolve("StudentInstructionMaterials"));
    +125                Files.createDirectories(studentFolder.resolve("StudentVisionAssessments"));
    +126                Path omnibus = studentFolder.resolve("omnibusDatabase.csv");
    +127                if (!Files.exists(omnibus)) {
    +128                    Files.createFile(omnibus);
    +129                }
    +130            } catch (IOException ioe) {
    +131                LOG.debug("Unable to create per-student folder or omnibus file for {}", name, ioe);
    +132            }
    +133        }
    +134    }
    +135
    +136    /**
    +137     * Make a filesystem-safe folder name by stripping or replacing forbidden
    +138     * characters.
    +139     */
    +140    private static String sanitize(final String s) {
    +141        if (s == null) {
    +142            return "";
    +143        }
    +144        String t = s.trim();
    +145        // remove control characters (newline, carriage return, etc.)
    +146        t = t.replaceAll("[\\p{Cntrl}]", "");
    +147        // replace common filesystem-forbidden characters with underscore
    +148        char[] forbidden = new char[]{'<','>',';',':','"','/','\\','|','?','*'};
    +149        for (char c : forbidden) {
    +150            t = t.replace(c, '_');
    +151        }
    +152        // collapse runs of whitespace into single space
    +153        t = t.replaceAll("\\s+", " ").trim();
    +154        // prevent names that are just dots
    +155        if (t.matches("^[.]+$")) {
    +156            t = "_";
    +157        }
    +158        return t;
    +159    }
    +160
    +161    /**
    +162     * Public safe name helper for filesystem paths. Mirrors the internal
    +163     * sanitize implementation but is callable from other packages.
    +164     *
    +165     * @param s input display name
    +166     * @return sanitized filesystem-safe name (never null)
    +167     */
    +168    public static String safeName(final String s) {
    +169        if (s == null) {
    +170            return "";
    +171        }
    +172        return sanitize(s);
    +173    }
    +174
    +175    /**
    +176     * Find the latest PNG plot file for a named student with the given prefix.
    +177     * Returns null when no matching files exist.
    +178    *
    +179    * @param studentName display name of student
    +180    * @param prefix file prefix such as "iOS" or "ScreenReader"
    +181    * @return path to the most recently modified matching PNG, or null
    +182     */
    +183    public static java.nio.file.Path latestPlotPath(final String studentName, final String prefix) {
    +184        if (studentName == null || studentName.trim().isEmpty()) {
    +185            return null;
    +186        }
    +187        java.nio.file.Path dir = studentPlotsDir(studentName);
    +188        if (!java.nio.file.Files.exists(dir)) {
    +189            return null;
    +190        }
    +191        java.nio.file.Path latest = null;
    +192        try (java.nio.file.DirectoryStream<java.nio.file.Path> ds = java.nio.file.Files.newDirectoryStream(dir, prefix + "-*.png")) {
    +193            for (java.nio.file.Path p : ds) {
    +194                try {
    +195                    if (latest == null) {
    +196                        latest = p;
    +197                    } else {
    +198                        java.nio.file.attribute.FileTime t1 = java.nio.file.Files.getLastModifiedTime(p);
    +199                        java.nio.file.attribute.FileTime t2 = java.nio.file.Files.getLastModifiedTime(latest);
    +200                        if (t1.compareTo(t2) > 0) {
    +201                            latest = p;
    +202                        }
    +203                    }
    +204                } catch (IOException ioe) {
    +205                    LOG.debug("Error reading file metadata for {}", p, ioe);
    +206                }
    +207            }
    +208        } catch (IOException ioe) {
    +209            LOG.debug("Error listing plot directory {}", dir, ioe);
    +210        }
    +211        return latest;
    +212    }
    +213
    +214    /**
    +215     * Return the per-student plots directory path (APP_HOME/StudentDataFiles/{safeName}/plots).
    +216     *
    +217     * @param studentName display name of the student
    +218     * @return path to the student's plots directory (never null)
    +219     */
    +220    public static java.nio.file.Path studentPlotsDir(final String studentName) {
    +221        return APP_HOME.resolve("StudentDataFiles").resolve(safeName(studentName)).resolve("plots");
    +222    }
    +223
    +224    /**
    +225     * Return the per-student reports directory path (APP_HOME/StudentDataFiles/{safeName}/reports).
    +226     *
    +227     * @param studentName display name of the student
    +228     * @return path to the student's reports directory (never null)
    +229     */
    +230    public static java.nio.file.Path studentReportsDir(final String studentName) {
    +231        return APP_HOME.resolve("StudentDataFiles").resolve(safeName(studentName)).resolve("reports");
    +232    }
    +233
    +234    /**
    +235     * Return the per-student collected data directory path (APP_HOME/StudentDataFiles/{safeName}/collected_data).
    +236     *
    +237     * @param studentName display name of the student
    +238     * @return path to the student's collected data directory (never null)
    +239     */
    +240    public static java.nio.file.Path studentCollectedDataDir(final String studentName) {
    +241        return APP_HOME.resolve("StudentDataFiles").resolve(safeName(studentName)).resolve("collected_data");
    +242    }
    +243
    +244    /**
    +245     * Attempt to return a simple list of students from PROJECT_ROOT/json_Files/students.json.
    +246     * Falls back to a single 'Test Student' entry when the file is missing or cannot be read.
    +247     *
    +248     * @return list of student display names (never null)
    +249     */
    +250    public static List<String> getStudents() {
    +251        // Attempt to read a simple students.json in PROJECT_ROOT/json_Files/students.json
    +252        List<String> list = new ArrayList<>();
    +253        Path p = PROJECT_ROOT.resolve("json_Files").resolve("students.json");
    +254        if (Files.exists(p)) {
    +255            try {
    +256                String text = Files.readString(p);
    +257                // try to isolate the array portion if present
    +258                int start = text.indexOf('[');
    +259                int end = text.lastIndexOf(']');
    +260                String body = (start >= 0 && end > start) ? text.substring(start, end + 1) : text;
    +261                java.util.regex.Pattern pat = java.util.regex.Pattern.compile("\"([^\"]+)\"");
    +262                java.util.regex.Matcher m = pat.matcher(body);
    +263                while (m.find()) {
    +264                    String candidate = m.group(1).trim();
    +265                    if (!candidate.isEmpty()) {
    +266                        list.add(candidate);
    +267                    }
    +268                }
    +269            } catch (IOException ioe) {
    +270                LOG.debug("Unable to read students.json {}", p, ioe);
    +271            }
    +272        }
    +273        if (list.isEmpty()) {
    +274            // fallback roster
    +275            list.add("Test Student");
    +276        }
    +277        return list;
    +278    }
    +279
    +280    /**
    +281     * Return the default student to use when none is provided by the caller.
    +282     * This is the first entry from getStudents() or a sensible fallback when
    +283     * the roster is empty.
    +284     *
    +285     * @return display name of the default student (never null)
    +286     */
    +287    public static String defaultStudent() {
    +288        /**
    +289         * Note: UI pages use this helper to provide a non-null default student
    +290         * when constructed with a null/empty student name so that charts and
    +291         * page logic can operate without requiring an immediate user selection.
    +292         */
    +293        List<String> s = getStudents();
    +294        if (s == null || s.isEmpty()) {
    +295            return "Demo Student";
    +296        }
    +297        return s.get(0);
    +298    }
    +299}
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    + + diff --git a/target/site/apidocs/src-html/com/studentgui/apphelpers/PythonPlotter.html b/target/site/apidocs/src-html/com/studentgui/apphelpers/PythonPlotter.html new file mode 100644 index 0000000..042a639 --- /dev/null +++ b/target/site/apidocs/src-html/com/studentgui/apphelpers/PythonPlotter.html @@ -0,0 +1,163 @@ + + + + +Source code + + + + + + +
    +
    +
    001package com.studentgui.apphelpers;
    +002
    +003import java.io.BufferedReader;
    +004import java.io.InputStreamReader;
    +005import java.nio.file.Path;
    +006import java.util.function.Consumer;
    +007
    +008import org.slf4j.Logger;
    +009import org.slf4j.LoggerFactory;
    +010
    +011/**
    +012 * Helper to invoke the repository's Python plot runner asynchronously.
    +013 * <p>
    +014 * This wrapper launches the repository's Python runner script in a
    +015 * background thread and collects its combined output. It is used by some
    +016 * legacy pages; newer pages prefer the Java-based charting helpers.
    +017 * </p>
    +018 */
    +019public class PythonPlotter {
    +020    private static final Logger LOG = LoggerFactory.getLogger(PythonPlotter.class);
    +021
    +022    /**
    +023     * Run the python runner for the given module and student name in a background thread.
    +024     * The {@code onComplete} consumer receives combined stdout/stderr text when the
    +025     * process finishes.
    +026     *
    +027     * @param moduleName module identifier passed to the python runner (non-null)
    +028     * @param studentName student display name used by the plotter (non-null)
    +029     * @param onComplete optional consumer receiving process output when complete; may be null
    +030     */
    +031    public static void runPlotAsync(final String moduleName, final String studentName, final Consumer<String> onComplete) {
    +032        if (studentName == null || studentName.trim().isEmpty()) {
    +033            String msg = "No student selected for plot generation";
    +034            LOG.warn(msg);
    +035            if (onComplete != null) {
    +036                onComplete.accept(msg);
    +037            }
    +038            return;
    +039        }
    +040
    +041        Path script = Helpers.PROJECT_ROOT.resolve("appPages").resolve("run_plot.py");
    +042
    +043        Thread t = new Thread(() -> {
    +044            StringBuilder out = new StringBuilder();
    +045            try {
    +046                ProcessBuilder pb = new ProcessBuilder("python", script.toString(), moduleName, studentName);
    +047                pb.directory(Helpers.PROJECT_ROOT.toFile());
    +048                pb.redirectErrorStream(true);
    +049                Process p = pb.start();
    +050                try (BufferedReader r = new BufferedReader(new InputStreamReader(p.getInputStream()))) {
    +051                    String line;
    +052                    while ((line = r.readLine()) != null) {
    +053                        out.append(line).append(System.lineSeparator());
    +054                    }
    +055                }
    +056                int rc = p.waitFor();
    +057                out.append("Exit code: ").append(rc).append(System.lineSeparator());
    +058            } catch (java.io.IOException | InterruptedException e) {
    +059                LOG.error("Error running python plot runner", e);
    +060                out.append("Error: ").append(e.toString()).append(System.lineSeparator());
    +061                if (e instanceof InterruptedException) {
    +062                    Thread.currentThread().interrupt();
    +063                }
    +064            }
    +065            if (onComplete != null) {
    +066                try {
    +067                    onComplete.accept(out.toString());
    +068                } catch (Exception ex) {
    +069                    // Log and continue: we don't want a faulty consumer to terminate
    +070                    // the worker thread unexpectedly. Avoid catching Error/Throwable.
    +071                    LOG.warn("onComplete consumer threw an exception; continuing. Output length={}", out.length(), ex);
    +072                }
    +073            }
    +074    }, "PythonPlotter-" + moduleName);
    +075        t.setDaemon(true);
    +076        t.start();
    +077    }
    +078
    +079    /**
    +080     * Private constructor to prevent instantiation of this helper class.
    +081     */
    +082    private PythonPlotter() {
    +083        // utility only
    +084    }
    +085}
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    + + diff --git a/target/site/apidocs/src-html/com/studentgui/apphelpers/SessionJsonWriter.html b/target/site/apidocs/src-html/com/studentgui/apphelpers/SessionJsonWriter.html new file mode 100644 index 0000000..d14d203 --- /dev/null +++ b/target/site/apidocs/src-html/com/studentgui/apphelpers/SessionJsonWriter.html @@ -0,0 +1,203 @@ + + + + +Source code + + + + + + +
    +
    +
    001package com.studentgui.apphelpers;
    +002
    +003import java.io.IOException;
    +004import java.nio.file.Files;
    +005import java.nio.file.Path;
    +006import java.time.Instant;
    +007import java.time.ZoneId;
    +008import java.time.format.DateTimeFormatter;
    +009import java.util.HashMap;
    +010import java.util.Map;
    +011
    +012import org.slf4j.Logger;
    +013import org.slf4j.LoggerFactory;
    +014
    +015import com.fasterxml.jackson.databind.ObjectMapper;
    +016
    +017/**
    +018 * Helper to write per-session JSON exports for app pages.
    +019 */
    +020public final class SessionJsonWriter {
    +021    private static final Logger LOG = LoggerFactory.getLogger(SessionJsonWriter.class);
    +022    private static final ObjectMapper MAPPER = new ObjectMapper();
    +023
    +024    private SessionJsonWriter() {}
    +025
    +026    /**
    +027     * Write a per-session JSON file into the student's StudentDataFiles folder.
    +028     * The filename will include a unix timestamp to ensure uniqueness per session.
    +029     *
    +030     * @param student display name of the student
    +031     * @param pageName short page identifier (e.g. "Abacus")
    +032     * @param payload arbitrary payload object to serialize (Map or POJO)
    +033     * @return the path to the written file, or null on failure
    +034     */
    +035    public static Path writeSessionJson(final String student, final String pageName, final Object payload) {
    +036        return writeSessionJson(student, pageName, payload, null);
    +037    }
    +038
    +039    /**
    +040     * Write a per-session JSON file and optionally include an explicit sessionId.
    +041     * If the explicit sessionId is null, this method will look for a "sessionId"
    +042     * entry inside the payload Map and use that if present. The envelope written
    +043     * to disk will include the sessionId when available.
    +044     *
    +045    * Filename format: {@code PageName-<epoch>-<readable>[-session-<sessionId>].json}
    +046     *
    +047     * @param student display name of the student
    +048     * @param pageName short page identifier (e.g. "Abacus")
    +049     * @param payload arbitrary payload object to serialize (Map or POJO)
    +050     * @param explicitSessionId optional session id to use in the envelope and filename
    +051     * @return the path to the written file, or null on failure
    +052     */
    +053    public static Path writeSessionJson(final String student, final String pageName, final Object payload, final String explicitSessionId) {
    +054        if (student == null || student.trim().isEmpty() || pageName == null) {
    +055            return null;
    +056        }
    +057        try {
    +058            Path outDir = Helpers.studentCollectedDataDir(student);
    +059            Files.createDirectories(outDir);
    +060            long ts = Instant.now().toEpochMilli();
    +061            // format for readability too
    +062            String readable = DateTimeFormatter.ofPattern("yyyyMMdd-HHmmssSSS").withZone(ZoneId.systemDefault()).format(Instant.ofEpochMilli(ts));
    +063
    +064            // Determine sessionId preference: explicit param first, then payload if it implements SessionPayload
    +065            String sid = explicitSessionId;
    +066            if (sid == null && payload instanceof com.studentgui.apphelpers.dto.SessionPayload) {
    +067                int s = ((com.studentgui.apphelpers.dto.SessionPayload) payload).getSessionId();
    +068                if (s != 0) {
    +069                    sid = Integer.toString(s);
    +070                }
    +071            }
    +072
    +073            String filename = String.format("%s-%d-%s%s.json", pageName, ts, readable, (sid != null ? "-session-" + sid : ""));
    +074            Path outFile = outDir.resolve(filename);
    +075
    +076            Map<String, Object> envelope = new HashMap<>();
    +077            envelope.put("student", student);
    +078            envelope.put("timestamp", ts);
    +079            envelope.put("timestampIso", readable);
    +080            envelope.put("page", pageName);
    +081            if (sid != null) {
    +082                envelope.put("sessionId", sid);
    +083            }
    +084            envelope.put("payload", payload);
    +085
    +086            byte[] bytes = MAPPER.writerWithDefaultPrettyPrinter().writeValueAsBytes(envelope);
    +087            Files.write(outFile, bytes);
    +088            LOG.info("Wrote session JSON for {} page {} to {}", student, pageName, outFile);
    +089            return outFile;
    +090        } catch (IOException ex) {
    +091            LOG.warn("Unable to write session JSON for {} page {}: {}", student, pageName, ex.toString());
    +092            return null;
    +093        }
    +094    }
    +095
    +096    /**
    +097     * Convenience overload that accepts an int sessionId to avoid callers
    +098     * converting to String. Delegates to the string-based overload.
    +099     *
    +100     * @param student display name of the student
    +101     * @param pageName short page identifier
    +102     * @param payload arbitrary payload object
    +103     * @param explicitSessionId numeric session id
    +104     * @return written file path or null
    +105     */
    +106    public static Path writeSessionJson(final String student, final String pageName, final Object payload, final int explicitSessionId) {
    +107        return writeSessionJson(student, pageName, payload, Integer.toString(explicitSessionId));
    +108    }
    +109
    +110    /**
    +111     * Backwards-compatible convenience method for callers that still have
    +112     * (codes,scores) arrays. It wraps them in a small Map and delegates to
    +113     * the main payload-based writer.
    +114     *
    +115     * @param student the student's display name
    +116     * @param pageName short page identifier (e.g. "Abacus")
    +117     * @param codes array of part codes to include in the payload
    +118     * @param scores array of scores corresponding to the codes
    +119     * @return path to the written JSON file, or null on failure
    +120     */
    +121    public static Path writeSessionJson(final String student, final String pageName, final String[] codes, final int[] scores) {
    +122        com.studentgui.apphelpers.dto.AssessmentPayload payload = new com.studentgui.apphelpers.dto.AssessmentPayload(0, codes, scores);
    +123        return writeSessionJson(student, pageName, payload);
    +124    }
    +125}
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    + + diff --git a/target/site/apidocs/src-html/com/studentgui/apphelpers/Settings.html b/target/site/apidocs/src-html/com/studentgui/apphelpers/Settings.html new file mode 100644 index 0000000..8361402 --- /dev/null +++ b/target/site/apidocs/src-html/com/studentgui/apphelpers/Settings.html @@ -0,0 +1,135 @@ + + + + +Source code + + + + + + +
    +
    +
    001package com.studentgui.apphelpers;
    +002
    +003import java.io.IOException;
    +004import java.io.InputStream;
    +005import java.io.OutputStream;
    +006import java.nio.file.Files;
    +007import java.nio.file.Path;
    +008import java.util.Properties;
    +009
    +010/**
    +011 * Lightweight settings persistence for simple key/value preferences.
    +012 */
    +013public final class Settings {
    +014    private static final Path SETTINGS_FILE = Helpers.APP_HOME.resolve("app.properties");
    +015    private static final Properties props = new Properties();
    +016    private static final org.slf4j.Logger LOG = org.slf4j.LoggerFactory.getLogger(Settings.class);
    +017
    +018    static {
    +019        // load existing if present
    +020        try (InputStream in = Files.exists(SETTINGS_FILE) ? Files.newInputStream(SETTINGS_FILE) : null) {
    +021            if (in != null) {
    +022                props.load(in);
    +023            }
    +024        } catch (IOException ioe) {
    +025            LOG.debug("Could not load settings from {}", SETTINGS_FILE, ioe);
    +026        }
    +027    }
    +028
    +029    private Settings() { throw new AssertionError(); }
    +030
    +031    /**
    +032     * Get a persisted setting value or return a default when missing.
    +033     *
    +034     * @param key setting key
    +035     * @param def default value when key is absent
    +036     * @return stored value or default
    +037     */
    +038    public static String get(final String key, final String def) {
    +039        return props.getProperty(key, def);
    +040    }
    +041
    +042    /**
    +043     * Store a setting value and persist to disk immediately.
    +044     *
    +045     * @param key setting key
    +046     * @param value setting value (null treated as empty string)
    +047     */
    +048    public static void put(final String key, final String value) {
    +049        props.setProperty(key, value == null ? "" : value);
    +050        // persist immediately
    +051        try (OutputStream out = Files.newOutputStream(SETTINGS_FILE)) {
    +052            props.store(out, "application settings");
    +053        } catch (IOException ioe) {
    +054            LOG.debug("Could not persist settings to {}", SETTINGS_FILE, ioe);
    +055        }
    +056    }
    +057}
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    + + diff --git a/target/site/apidocs/src-html/com/studentgui/apphelpers/SqlGenerate.html b/target/site/apidocs/src-html/com/studentgui/apphelpers/SqlGenerate.html new file mode 100644 index 0000000..16803f5 --- /dev/null +++ b/target/site/apidocs/src-html/com/studentgui/apphelpers/SqlGenerate.html @@ -0,0 +1,268 @@ + + + + +Source code + + + + + + +
    +
    +
    001package com.studentgui.apphelpers;
    +002
    +003import java.io.IOException;
    +004import java.nio.file.Files;
    +005import java.nio.file.Path;
    +006import java.sql.Connection;
    +007import java.sql.DriverManager;
    +008import java.sql.SQLException;
    +009import java.sql.Statement;
    +010
    +011import org.slf4j.Logger;
    +012import org.slf4j.LoggerFactory;
    +013
    +014/**
    +015 * SQL schema generator for the normalized application database.
    +016 *
    +017 * This class ensures the SQLite database file exists and creates the
    +018 * canonical tables used by the application. Safe to call repeatedly on
    +019 * application startup.
    +020 */
    +021/**
    +022 * Utility responsible for creating/validating the on-disk SQLite database
    +023 * and canonical schema used by the application. Safe to call multiple times.
    +024 */
    +025public class SqlGenerate {
    +026    private static final Path DB = Helpers.DATABASE_PATH;
    +027    private static final Logger LOG = LoggerFactory.getLogger(SqlGenerate.class);
    +028    // Ported schema from Python appHelpers/sqlgenerate.py
    +029    private static final String[] SCHEMA = new String[] {
    +030        // Core student table
    +031        """
    +032        CREATE TABLE IF NOT EXISTS Student (
    +033            id INTEGER PRIMARY KEY AUTOINCREMENT,
    +034            name TEXT NOT NULL,
    +035            birthdate TEXT,
    +036            notes TEXT
    +037        );
    +038        """,
    +039        // ProgressType
    +040        """
    +041        CREATE TABLE IF NOT EXISTS ProgressType (
    +042            id INTEGER PRIMARY KEY AUTOINCREMENT,
    +043            name TEXT NOT NULL UNIQUE,
    +044            description TEXT
    +045        );
    +046        """,
    +047        // ProgressSession
    +048        """
    +049        CREATE TABLE IF NOT EXISTS ProgressSession (
    +050            id INTEGER PRIMARY KEY AUTOINCREMENT,
    +051            student_id INTEGER NOT NULL,
    +052            progress_type_id INTEGER NOT NULL,
    +053            date TEXT NOT NULL,
    +054            notes TEXT,
    +055            FOREIGN KEY(student_id) REFERENCES Student(id) ON DELETE CASCADE,
    +056            FOREIGN KEY(progress_type_id) REFERENCES ProgressType(id) ON DELETE CASCADE
    +057        );
    +058        """,
    +059        // KeyboardingResult
    +060        """
    +061        CREATE TABLE IF NOT EXISTS KeyboardingResult (
    +062            id INTEGER PRIMARY KEY AUTOINCREMENT,
    +063            session_id INTEGER NOT NULL,
    +064            program TEXT NOT NULL,
    +065            topic TEXT NOT NULL,
    +066            speed INTEGER NOT NULL,
    +067            accuracy INTEGER NOT NULL,
    +068            FOREIGN KEY(session_id) REFERENCES ProgressSession(id) ON DELETE CASCADE
    +069        );
    +070        """,
    +071        // TrialResult
    +072        """
    +073        CREATE TABLE IF NOT EXISTS TrialResult (
    +074            id INTEGER PRIMARY KEY AUTOINCREMENT,
    +075            session_id INTEGER NOT NULL,
    +076            task TEXT NOT NULL,
    +077            lesson TEXT,
    +078            session_label TEXT,
    +079            trial_number INTEGER NOT NULL,
    +080            score INTEGER,
    +081            FOREIGN KEY(session_id) REFERENCES ProgressSession(id) ON DELETE CASCADE
    +082        );
    +083        """,
    +084        // TrialSessionSummary
    +085        """
    +086        CREATE TABLE IF NOT EXISTS TrialSessionSummary (
    +087            id INTEGER PRIMARY KEY AUTOINCREMENT,
    +088            session_id INTEGER NOT NULL UNIQUE,
    +089            median FLOAT,
    +090            notes TEXT,
    +091            FOREIGN KEY(session_id) REFERENCES ProgressSession(id) ON DELETE CASCADE
    +092        );
    +093        """,
    +094        // AssessmentPart
    +095        """
    +096        CREATE TABLE IF NOT EXISTS AssessmentPart (
    +097            id INTEGER PRIMARY KEY AUTOINCREMENT,
    +098            progress_type_id INTEGER NOT NULL,
    +099            code TEXT NOT NULL,
    +100            description TEXT,
    +101            UNIQUE(progress_type_id, code),
    +102            FOREIGN KEY(progress_type_id) REFERENCES ProgressType(id) ON DELETE CASCADE
    +103        );
    +104        """,
    +105        // AssessmentResult
    +106        """
    +107        CREATE TABLE IF NOT EXISTS AssessmentResult (
    +108            id INTEGER PRIMARY KEY AUTOINCREMENT,
    +109            session_id INTEGER NOT NULL,
    +110            part_id INTEGER NOT NULL,
    +111            score INTEGER,
    +112            FOREIGN KEY(session_id) REFERENCES ProgressSession(id) ON DELETE CASCADE,
    +113            FOREIGN KEY(part_id) REFERENCES AssessmentPart(id) ON DELETE CASCADE
    +114        );
    +115        """
    +116        ,
    +117        // ContactLog details tied to a ProgressSession
    +118        """
    +119        CREATE TABLE IF NOT EXISTS ContactLog (
    +120            id INTEGER PRIMARY KEY AUTOINCREMENT,
    +121            session_id INTEGER NOT NULL,
    +122            student_name TEXT,
    +123            date TEXT,
    +124            guardian_name TEXT,
    +125            contact_method TEXT,
    +126            phone_number TEXT,
    +127            email_address TEXT,
    +128            contact_response TEXT,
    +129            contact_general TEXT,
    +130            contact_specific TEXT,
    +131            contact_notes TEXT,
    +132            FOREIGN KEY(session_id) REFERENCES ProgressSession(id) ON DELETE CASCADE
    +133        );
    +134        """
    +135    };
    +136
    +137    /**
    +138     * Ensure the database file and canonical schema exist. This method is idempotent
    +139     * and safe to call on application startup. It will create the parent folder
    +140     * for the DB file if necessary and apply the embedded SCHEMA statements.
    +141     */
    +142    public static void initializeDatabase() {
    +143        try {
    +144            Path parent = DB.getParent();
    +145            if (parent != null && !Files.exists(parent)) {
    +146                Files.createDirectories(parent);
    +147            }
    +148            if (Files.exists(DB) && Files.isDirectory(DB)) {
    +149                LOG.error("Path is a directory, cannot create DB file: {}", DB);
    +150                return;
    +151            }
    +152            if (Files.exists(DB)) {
    +153                LOG.info("Database already exists at {}", DB);
    +154                // even if the DB exists, ensure schema is present by connecting and executing schema statements
    +155            }
    +156            // create/connect to SQLite database file by opening a connection
    +157            String url = "jdbc:sqlite:" + DB.toString();
    +158            try (Connection conn = DriverManager.getConnection(url)) {
    +159                if (conn != null) {
    +160                    executeSchema(conn);
    +161                }
    +162            }
    +163            LOG.info("Database initialized/validated at {}", DB);
    +164        } catch (SQLException | IOException e) {
    +165            LOG.error("Error initializing database", e);
    +166        }
    +167    }
    +168
    +169    /**
    +170     * Execute the SCHEMA statements on the provided connection.
    +171     * Extracted to make the schema application clearer and easier to test.
    +172     *
    +173     * @param conn established JDBC connection to the target SQLite DB
    +174     * @throws SQLException if applying any schema statement fails
    +175     */
    +176    private static void executeSchema(final Connection conn) throws SQLException {
    +177        try (Statement st = conn.createStatement()) {
    +178            for (String sql : SCHEMA) {
    +179                st.execute(sql);
    +180            }
    +181        }
    +182    }
    +183
    +184    /**
    +185     * Private constructor to prevent instantiation of this utility class.
    +186     */
    +187    private SqlGenerate() {
    +188        throw new AssertionError("Not instantiable");
    +189    }
    +190}
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    + + diff --git a/target/site/apidocs/src-html/com/studentgui/apphelpers/UiNotifier.html b/target/site/apidocs/src-html/com/studentgui/apphelpers/UiNotifier.html new file mode 100644 index 0000000..2ce735f --- /dev/null +++ b/target/site/apidocs/src-html/com/studentgui/apphelpers/UiNotifier.html @@ -0,0 +1,139 @@ + + + + +Source code + + + + + + +
    +
    +
    001package com.studentgui.apphelpers;
    +002
    +003import java.awt.BorderLayout;
    +004import java.awt.Color;
    +005import java.awt.Font;
    +006
    +007import javax.swing.JLabel;
    +008import javax.swing.JWindow;
    +009import javax.swing.SwingUtilities;
    +010
    +011/**
    +012 * Very small non-modal notification window for quick status messages.
    +013 *
    +014 * Lightweight utility used across pages to display transient, non-blocking
    +015 * notifications to the user.
    +016 */
    +017public class UiNotifier {
    +018    private static JWindow window;
    +019    private static final org.slf4j.Logger LOG = org.slf4j.LoggerFactory.getLogger(UiNotifier.class);
    +020
    +021    /**
    +022     * Display a short, transient notification message on screen.
    +023     *
    +024     * @param message message text to display
    +025     */
    +026    public static void show(final String message) {
    +027        SwingUtilities.invokeLater(() -> {
    +028            if (window != null) {
    +029                window.dispose();
    +030            }
    +031            window = new JWindow();
    +032            JLabel label = new JLabel(message);
    +033            label.setOpaque(true);
    +034            label.setBackground(new Color(0x22, 0x22, 0x22, 200));
    +035            label.setForeground(Color.WHITE);
    +036            label.setFont(new Font(Font.SANS_SERIF, Font.PLAIN, 12));
    +037            window.getContentPane().setLayout(new BorderLayout());
    +038            window.getContentPane().add(label, BorderLayout.CENTER);
    +039            window.pack();
    +040            window.setAlwaysOnTop(true);
    +041            window.setLocationRelativeTo(null);
    +042            window.setVisible(true);
    +043            // auto-hide after 2 seconds
    +044            new Thread(() -> {
    +045                try { Thread.sleep(2000); }
    +046                catch (InterruptedException ie) { LOG.debug("UiNotifier sleep interrupted", ie); Thread.currentThread().interrupt(); }
    +047                SwingUtilities.invokeLater(() -> { if (window != null) { window.dispose(); window = null; } });
    +048            }).start();
    +049        });
    +050    }
    +051    
    +052    // Note: UiNotifier.show is intentionally lightweight and non-blocking;
    +053    // the implemented method above contains the behavior and JavaDoc.
    +054
    +055    /**
    +056     * Private constructor to prevent instantiation.
    +057     */
    +058    private UiNotifier() {
    +059        // utility only
    +060    }
    +061}
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    + + diff --git a/target/site/apidocs/src-html/com/studentgui/apphelpers/dto/AssessmentPayload.html b/target/site/apidocs/src-html/com/studentgui/apphelpers/dto/AssessmentPayload.html new file mode 100644 index 0000000..0b90ad2 --- /dev/null +++ b/target/site/apidocs/src-html/com/studentgui/apphelpers/dto/AssessmentPayload.html @@ -0,0 +1,119 @@ + + + + +Source code + + + + + + +
    +
    +
    001package com.studentgui.apphelpers.dto;
    +002
    +003import java.util.Arrays;
    +004
    +005/**
    +006 * Typed payload for assessment-style pages (codes + scores).
    +007 */
    +008public class AssessmentPayload implements SessionPayload {
    +009    /** Database session id for this payload. */
    +010    public int sessionId;
    +011    /** Array of part codes (e.g. "P1_1"). */
    +012    public String[] codes;
    +013    /** Parallel array of integer scores. */
    +014    public int[] scores;
    +015
    +016    /** No-arg constructor for Jackson and tests. */
    +017    public AssessmentPayload() {}
    +018
    +019    /**
    +020     * Create an assessment payload.
    +021     *
    +022    * @param sessionIdParam numeric DB session id
    +023    * @param codesParam array of part codes
    +024    * @param scoresParam array of scores
    +025     */
    +026    public AssessmentPayload(final int sessionIdParam, final String[] codesParam, final int[] scoresParam) {
    +027        this.sessionId = sessionIdParam;
    +028        this.codes = codesParam;
    +029        this.scores = scoresParam;
    +030    }
    +031
    +032    @Override
    +033    public int getSessionId() {
    +034        return this.sessionId;
    +035    }
    +036
    +037    @Override
    +038    public String toString() {
    +039        return "AssessmentPayload{sessionId=" + sessionId + ", codes=" + Arrays.toString(codes) + ", scores=" + Arrays.toString(scores) + "}";
    +040    }
    +041}
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    + + diff --git a/target/site/apidocs/src-html/com/studentgui/apphelpers/dto/ContactPayload.html b/target/site/apidocs/src-html/com/studentgui/apphelpers/dto/ContactPayload.html new file mode 100644 index 0000000..1004f87 --- /dev/null +++ b/target/site/apidocs/src-html/com/studentgui/apphelpers/dto/ContactPayload.html @@ -0,0 +1,134 @@ + + + + +Source code + + + + + + +
    +
    +
    001package com.studentgui.apphelpers.dto;
    +002
    +003/**
    +004 * Typed payload for contact log entries.
    +005 */
    +006public class ContactPayload implements SessionPayload {
    +007    /** Database session id. */
    +008    public int sessionId;
    +009    /** Guardian/parent name. */
    +010    public String guardian;
    +011    /** Method of contact (Phone/Email/etc). */
    +012    public String method;
    +013    /** Phone number. */
    +014    public String phone;
    +015    /** Email address. */
    +016    public String email;
    +017    /** Brief response summary. */
    +018    public String response;
    +019    /** High-level general notes. */
    +020    public String general;
    +021    /** Specific action items or points. */
    +022    public String specific;
    +023    /** Full notes text. */
    +024    public String notes;
    +025
    +026    /** No-arg constructor for Jackson. */
    +027    public ContactPayload() {}
    +028
    +029    /**
    +030     * Create a contact payload.
    +031     *
    +032    * @param sessionIdParam database session id
    +033    * @param guardianParam guardian/parent name
    +034    * @param methodParam method of contact (Phone/Email/etc)
    +035    * @param phoneParam phone number
    +036    * @param emailParam email address
    +037    * @param responseParam brief response summary
    +038    * @param generalParam high-level general notes
    +039    * @param specificParam specific action items or points
    +040    * @param notesParam full notes text
    +041     */
    +042    public ContactPayload(final int sessionIdParam, final String guardianParam, final String methodParam, final String phoneParam, final String emailParam, final String responseParam, final String generalParam, final String specificParam, final String notesParam) {
    +043        this.sessionId = sessionIdParam;
    +044        this.guardian = guardianParam;
    +045        this.method = methodParam;
    +046        this.phone = phoneParam;
    +047        this.email = emailParam;
    +048        this.response = responseParam;
    +049        this.general = generalParam;
    +050        this.specific = specificParam;
    +051        this.notes = notesParam;
    +052    }
    +053
    +054    @Override
    +055    public int getSessionId() { return this.sessionId; }
    +056}
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    + + diff --git a/target/site/apidocs/src-html/com/studentgui/apphelpers/dto/KeyboardingPayload.html b/target/site/apidocs/src-html/com/studentgui/apphelpers/dto/KeyboardingPayload.html new file mode 100644 index 0000000..b94c3a3 --- /dev/null +++ b/target/site/apidocs/src-html/com/studentgui/apphelpers/dto/KeyboardingPayload.html @@ -0,0 +1,118 @@ + + + + +Source code + + + + + + +
    +
    +
    001package com.studentgui.apphelpers.dto;
    +002
    +003/**
    +004 * Typed payload for Keyboarding page.
    +005 */
    +006public class KeyboardingPayload implements SessionPayload {
    +007    /** Database session id. */
    +008    public int sessionId;
    +009    /** Program or curriculum name. */
    +010    public String program;
    +011    /** Topic or lesson name. */
    +012    public String topic;
    +013    /** Speed in WPM. */
    +014    public int speed;
    +015    /** Accuracy percentage. */
    +016    public int accuracy;
    +017
    +018    /** No-arg constructor for Jackson. */
    +019    public KeyboardingPayload() {}
    +020
    +021    /**
    +022     * Create keyboarding payload.
    +023     *
    +024    * @param sessionIdParam DB session id
    +025    * @param programParam program name
    +026    * @param topicParam topic name
    +027    * @param speedParam words per minute
    +028    * @param accuracyParam percent accuracy
    +029     */
    +030    public KeyboardingPayload(final int sessionIdParam, final String programParam, final String topicParam, final int speedParam, final int accuracyParam) {
    +031        this.sessionId = sessionIdParam;
    +032        this.program = programParam;
    +033        this.topic = topicParam;
    +034        this.speed = speedParam;
    +035        this.accuracy = accuracyParam;
    +036    }
    +037
    +038    @Override
    +039    public int getSessionId() { return this.sessionId; }
    +040}
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    + + diff --git a/target/site/apidocs/src-html/com/studentgui/apphelpers/dto/NotesPayload.html b/target/site/apidocs/src-html/com/studentgui/apphelpers/dto/NotesPayload.html new file mode 100644 index 0000000..f5cb86c --- /dev/null +++ b/target/site/apidocs/src-html/com/studentgui/apphelpers/dto/NotesPayload.html @@ -0,0 +1,106 @@ + + + + +Source code + + + + + + +
    +
    +
    001package com.studentgui.apphelpers.dto;
    +002
    +003/**
    +004 * Typed payload for freeform notes pages.
    +005 */
    +006public class NotesPayload implements SessionPayload {
    +007    /** Database session id. */
    +008    public int sessionId;
    +009    /** The freeform notes text. */
    +010    public String notes;
    +011
    +012    /** No-arg constructor for Jackson. */
    +013    public NotesPayload() {}
    +014
    +015    /**
    +016     * Create a notes payload.
    +017     *
    +018    * @param sessionIdParam DB session id
    +019    * @param notesParam freeform notes
    +020     */
    +021    public NotesPayload(final int sessionIdParam, final String notesParam) {
    +022        this.sessionId = sessionIdParam;
    +023        this.notes = notesParam;
    +024    }
    +025
    +026    @Override
    +027    public int getSessionId() { return this.sessionId; }
    +028}
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    + + diff --git a/target/site/apidocs/src-html/com/studentgui/apphelpers/dto/SessionPayload.html b/target/site/apidocs/src-html/com/studentgui/apphelpers/dto/SessionPayload.html new file mode 100644 index 0000000..94433cb --- /dev/null +++ b/target/site/apidocs/src-html/com/studentgui/apphelpers/dto/SessionPayload.html @@ -0,0 +1,91 @@ + + + + +Source code + + + + + + +
    +
    +
    001package com.studentgui.apphelpers.dto;
    +002
    +003/**
    +004 * Common interface for session-scoped payloads that carry a DB session id.
    +005 */
    +006public interface SessionPayload {
    +007    /**
    +008     * Return the database session id associated with this payload.
    +009     *
    +010     * @return the database session id for this payload (may be 0 when unknown)
    +011     */
    +012    int getSessionId();
    +013}
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    + + diff --git a/target/site/apidocs/src-html/com/studentgui/apppages/Abacus.html b/target/site/apidocs/src-html/com/studentgui/apppages/Abacus.html new file mode 100644 index 0000000..49c8447 --- /dev/null +++ b/target/site/apidocs/src-html/com/studentgui/apppages/Abacus.html @@ -0,0 +1,520 @@ + + + + +Source code + + + + + + +
    +
    +
    001package com.studentgui.apppages;
    +002
    +003import java.awt.BorderLayout;
    +004import java.awt.Font;
    +005import java.awt.GridBagConstraints;
    +006import java.awt.GridBagLayout;
    +007import java.awt.Insets;
    +008import java.awt.event.ActionEvent;
    +009import java.awt.event.KeyEvent;
    +010import java.sql.SQLException;
    +011import java.time.LocalDate;
    +012
    +013import javax.swing.JButton;
    +014import javax.swing.JLabel;
    +015import javax.swing.JOptionPane;
    +016import javax.swing.JPanel;
    +017import javax.swing.JScrollPane;
    +018import javax.swing.SwingUtilities;
    +019
    +020import org.slf4j.Logger;
    +021import org.slf4j.LoggerFactory;
    +022
    +023/**
    +024 * Abacus computational skills assessment page.
    +025 *
    +026 * <p>Provides a structured interface for evaluating student proficiency with the Cranmer
    +027 * Abacus across 22 standardized skills organized into 8 progressive competency phases:</p>
    +028 *
    +029 * <ul>
    +030 *   <li><b>Phase 1 (P1_1–P1_4):</b> Foundational bead manipulation (setting, clearing, place value, vocabulary)</li>
    +031 *   <li><b>Phase 2 (P2_1–P2_3):</b> Single-digit addition (direct and indirect methods)</li>
    +032 *   <li><b>Phase 3 (P3_1–P3_3):</b> Single-digit subtraction (direct and indirect methods)</li>
    +033 *   <li><b>Phase 4 (P4_1–P4_2):</b> Multiplication with multi-digit operands</li>
    +034 *   <li><b>Phase 5 (P5_1–P5_2):</b> Division with multi-digit operands</li>
    +035 *   <li><b>Phase 6 (P6_1–P6_4):</b> Decimal arithmetic (all four operations)</li>
    +036 *   <li><b>Phase 7 (P7_1–P7_4):</b> Fraction arithmetic (all four operations)</li>
    +037 *   <li><b>Phase 8 (P8_1–P8_2):</b> Advanced operations (percentages, square roots)</li>
    +038 * </ul>
    +039 *
    +040 * <p><b>Data Persistence and Export:</b></p>
    +041 * <ul>
    +042 *   <li>Skill scores are captured via {@link com.studentgui.uicomp.PhaseScoreField} components (integer 0–4 typical)</li>
    +043 *   <li>Submit button persists values to normalized schema using {@link com.studentgui.apphelpers.Database#insertAssessmentResults}</li>
    +044 *   <li>Session data exported to timestamped JSON in {@code StudentDataFiles/<student>/Sessions/Abacus/}</li>
    +045 *   <li>Per-phase time-series plots generated and saved to {@code plots/} directory</li>
    +046 *   <li>Comprehensive Markdown and HTML reports generated with embedded phase plots and color-coded legends</li>
    +047 * </ul>
    +048 *
    +049 * <p><b>Report Artifacts:</b></p>
    +050 * <ul>
    +051 *   <li><b>JSON export:</b> {@code Abacus-<sessionId>-<timestamp>.json} with session envelope</li>
    +052 *   <li><b>Phase group plots:</b> {@code Abacus-<sessionId>-<date>-P<N>.png} (8 PNG images)</li>
    +053 *   <li><b>Markdown report:</b> {@code reports/Abacus-<sessionId>-<date>.md} with relative image links</li>
    +054 *   <li><b>HTML report:</b> {@code reports/Abacus-<sessionId>-<date>.html} with inline styles and legends</li>
    +055 * </ul>
    +056 *
    +057 * <p>The shared {@link JLineGraph} visualizes recent session trends, grouping skills by phase prefix
    +058 * to maintain chart readability. Implements {@link com.studentgui.app.DateChangeListener} and
    +059 * {@link com.studentgui.app.StudentChangeListener} for dynamic updates when global selections change.</p>
    +060 *
    +061 * @see com.studentgui.apphelpers.Database
    +062 * @see JLineGraph
    +063 * @see com.studentgui.uicomp.PhaseScoreField
    +064 */
    +065public class Abacus extends JPanel implements com.studentgui.app.DateChangeListener, com.studentgui.app.StudentChangeListener {
    +066    private static final Logger LOG = LoggerFactory.getLogger(Abacus.class);
    +067
    +068    /** Array of input components for each skill. */
    +069    private final com.studentgui.uicomp.PhaseScoreField[] skillFields;
    +070    /** Canonical list of abacus assessment parts: code and display label. */
    +071    private final String[][] parts;
    +072    /** Shared graph component used to visualize recent results. */
    +073    private final JLineGraph lineGraph; // Reference to the JLineGraph instance
    +074    /** Selected student display name (may be null). */
    +075    private String studentNameParam;
    +076    /** Session date associated with persisted progress. */
    +077    private LocalDate dateParam;
    +078    /**
    +079     * Title label shown at the top of the page.
    +080     */
    +081    private JLabel titleLabel;
    +082    /**
    +083     * Base title text used when rendering the page header (date suffixes are appended).
    +084     */
    +085    private final String baseTitle = "Abacus Skills Progression";
    +086
    +087    /**
    +088     * Construct the Abacus page for the given student and session date.
    +089     *
    +090     * @param studentName the selected student's display name (may be null before selection)
    +091     * @param date the date to associate with created progress sessions
    +092     * @param lineGraph the shared graph component used to visualize results
    +093     */
    +094    public Abacus(final String studentName, final LocalDate date, final JLineGraph lineGraph) {
    +095    this.studentNameParam = (studentName == null || studentName.trim().isEmpty()) ? com.studentgui.apphelpers.Helpers.defaultStudent() : studentName;
    +096        this.dateParam = date;
    +097        this.lineGraph = lineGraph; // Use the passed in graph instance
    +098        setLayout(new BorderLayout());
    +099
    +100        // Initialize skills array and layout using canonical abacus parts
    +101        this.parts = new String[][]{
    +102            {"P1_1","1.1 Setting Numbers"},{"P1_2","1.2 Clearing Beads"},{"P1_3","1.3 Place Value"},{"P1_4","1.4 Vocabulary"},
    +103            {"P2_1","2.1 Addition of Single Digit Numbers"},{"P2_2","2.2 Direct Addition"},{"P2_3","2.3 Indirect Addition"},
    +104            {"P3_1","3.1 Subtraction of Single Digit Numbers"},{"P3_2","3.2 Direct Subtraction"},{"P3_3","3.3 Indirect Subtraction"},
    +105            {"P4_1","4.1 Multiplication – 2+ Digit Multiplicand 1-Digit Multiplier"},{"P4_2","4.2 Multiplication – 2+ Digit Multiplicand AND Multiplier"},
    +106            {"P5_1","5.1 Division – 2+ Digit Dividend 1-Digit Divisor"},{"P5_2","5.2 Division – 2+ Digit Dividend AND 1 Digit Divisor"},
    +107            {"P6_1","6.1 Addition of Decimals"},{"P6_2","6.2 Subtraction of Decimals"},{"P6_3","6.3 Multiplication of Decimals"},{"P6_4","6.4 Division of Decimals"},
    +108            {"P7_1","7.1 Addition of Fractions"},{"P7_2","7.2 Subtraction of Fractions"},{"P7_3","7.3 Multiplication of Fractions"},{"P7_4","7.4 Division of Fractions"},
    +109            {"P8_1","8.1 Percent"},{"P8_2","8.2 Square Root"}
    +110        };
    +111
    +112        // Panel for data entry
    +113        JPanel dataEntryPanel = new JPanel();
    +114        dataEntryPanel.setLayout(new GridBagLayout());
    +115    JPanel view = new JPanel(new BorderLayout());
    +116    view.add(dataEntryPanel, BorderLayout.NORTH);
    +117    view.setBorder(javax.swing.BorderFactory.createEmptyBorder(20,20,20,20));
    +118    JScrollPane dataEntryScrollPane = new JScrollPane(view);
    +119    dataEntryScrollPane.setVerticalScrollBarPolicy(JScrollPane.VERTICAL_SCROLLBAR_AS_NEEDED);
    +120    dataEntryScrollPane.setHorizontalScrollBarPolicy(JScrollPane.HORIZONTAL_SCROLLBAR_NEVER);
    +121    dataEntryScrollPane.getAccessibleContext().setAccessibleName("Abacus data entry scroll pane");
    +122
    +123    GridBagConstraints gbc = new GridBagConstraints();
    +124    gbc.insets = new Insets(2, 2, 2, 2);
    +125        gbc.fill = GridBagConstraints.HORIZONTAL;
    +126        gbc.weightx = 1.0;
    +127        gbc.weighty = 0.0;
    +128
    +129    this.titleLabel = new JLabel(baseTitle);
    +130    this.titleLabel.setFont(this.titleLabel.getFont().deriveFont(Font.BOLD, 16));
    +131        gbc.gridx = 0;
    +132        gbc.gridy = 0;
    +133        gbc.gridwidth = GridBagConstraints.REMAINDER;
    +134        dataEntryPanel.add(titleLabel, gbc);
    +135
    +136        gbc.gridy = 1;
    +137        gbc.gridwidth = GridBagConstraints.REMAINDER;
    +138        gbc.ipady = 20;
    +139        dataEntryPanel.add(new JPanel(), gbc);
    +140
    +141    // visual spacing controlled by PhaseScoreField and layout
    +142
    +143    String[] labels = java.util.Arrays.stream(this.parts).map(x->x[1]).toArray(String[]::new);
    +144        int maxPx = com.studentgui.uicomp.PhaseScoreField.computeMaxLabelPixelWidth(titleLabel.getFont(), labels);
    +145        com.studentgui.uicomp.PhaseScoreField.setGlobalLabelWidth(Math.min(320, Math.max(140, maxPx + 50)));
    +146    skillFields = new com.studentgui.uicomp.PhaseScoreField[this.parts.length];
    +147    for (int i = 0; i < this.parts.length; i++) {
    +148            gbc.gridy = i + 2;
    +149            gbc.gridx = 0;
    +150            gbc.gridwidth = 1;
    +151            com.studentgui.uicomp.PhaseScoreField field = new com.studentgui.uicomp.PhaseScoreField(this.parts[i][1], 0);
    +152            field.setName("abacus_" + this.parts[i][0]);
    +153            field.getAccessibleContext().setAccessibleName(this.parts[i][1]);
    +154            field.setToolTipText("Enter a numeric score for " + this.parts[i][1]);
    +155            gbc.gridx = 0; gbc.gridwidth = 2; gbc.insets = new Insets(2, 2, 2, 2);
    +156            dataEntryPanel.add(field, gbc);
    +157            skillFields[i] = field;
    +158            gbc.gridx = 2; gbc.gridwidth = 1; gbc.insets = new Insets(2, 0, 2, 2);
    +159            dataEntryPanel.add(new JPanel(), gbc);
    +160        }
    +161
    +162    gbc.gridy = this.parts.length + 3;
    +163        gbc.gridx = 0;
    +164        gbc.gridwidth = GridBagConstraints.REMAINDER;
    +165        gbc.weighty = 1.0;
    +166        dataEntryPanel.add(new JPanel(), gbc);
    +167
    +168    // Place Submit and Open Latest side-by-side with IOS-like height
    +169    gbc.gridy = this.parts.length + 4;
    +170    gbc.weighty = 0.0;
    +171    gbc.gridx = 0;
    +172    gbc.gridwidth = 1;
    +173    JButton submitDataButton = new JButton("Submit Data");
    +174    submitDataButton.setPreferredSize(new java.awt.Dimension(0, 32));
    +175    submitDataButton.addActionListener((ActionEvent e) -> { submitData(); refreshGraph(); });
    +176    submitDataButton.setMnemonic(KeyEvent.VK_S);
    +177    submitDataButton.setToolTipText("Save Abacus scores for the selected student (Alt+S)");
    +178    submitDataButton.getAccessibleContext().setAccessibleName("Submit Abacus Data");
    +179    dataEntryPanel.add(submitDataButton, gbc);
    +180
    +181    gbc.gridx = 1;
    +182    JButton openLatestBtn = new JButton("Open Latest Plot");
    +183    openLatestBtn.setPreferredSize(new java.awt.Dimension(0, 32));
    +184    openLatestBtn.addActionListener((ActionEvent e) -> {
    +185        java.nio.file.Path p = com.studentgui.apphelpers.Helpers.latestPlotPath(this.studentNameParam, "Abacus");
    +186        if (p == null) {
    +187            com.studentgui.apphelpers.UiNotifier.show("No Abacus plot found for student");
    +188        } else {
    +189            try {
    +190                java.awt.Desktop.getDesktop().open(p.toFile());
    +191            } catch (java.io.IOException | UnsupportedOperationException | SecurityException ex) {
    +192                com.studentgui.apphelpers.UiNotifier.show("Unable to open plot: " + p.getFileName().toString());
    +193            }
    +194        }
    +195    });
    +196    dataEntryPanel.add(openLatestBtn, gbc);
    +197
    +198    gbc.gridx = 2; gbc.gridwidth = GridBagConstraints.REMAINDER;
    +199    dataEntryPanel.add(new JPanel(), gbc);
    +200
    +201        add(dataEntryScrollPane, BorderLayout.CENTER);
    +202
    +203        // Add existing graph reference
    +204        add(lineGraph, BorderLayout.SOUTH);
    +205
    +206        SwingUtilities.invokeLater(() -> {
    +207            dataEntryPanel.setPreferredSize(dataEntryPanel.getPreferredSize());
    +208            updateTitleDate();
    +209            revalidate();
    +210        });
    +211
    +212        // Ensure application folders and DB schema exist before DB operations
    +213        com.studentgui.apphelpers.Helpers.createFolderHierarchy();
    +214        initDatabase();
    +215        refreshGraph();
    +216    }
    +217
    +218    /**
    +219     * Ensure the canonical progress-type and assessment parts for Abacus exist
    +220     * in the normalized database schema. Safe to call multiple times.
    +221     */
    +222    private void initDatabase() {
    +223        try {
    +224            int ptId = com.studentgui.apphelpers.Database.getOrCreateProgressType("Abacus");
    +225            // Use the canonical part codes declared on this page so parts are created
    +226            // with the expected codes like "P1_1", "P1_2", ...
    +227            String[] codes = new String[this.parts.length];
    +228            for (int i = 0; i < this.parts.length; i++) {
    +229                codes[i] = this.parts[i][0];
    +230            }
    +231            com.studentgui.apphelpers.Database.ensureAssessmentParts(ptId, codes);
    +232            try {
    +233                com.studentgui.apphelpers.Database.cleanupAssessmentParts(ptId, codes);
    +234            } catch (SQLException se) {
    +235                LOG.warn("Could not cleanup legacy parts for Abacus", se);
    +236            }
    +237        } catch (SQLException e) {
    +238            LOG.error("SQL error initializing Abacus parts", e);
    +239        }
    +240    }
    +241
    +242    /**
    +243     * Read input fields, validate numeric input, and persist the values as a
    +244     * new progress session for the selected student.
    +245     */
    +246    private void submitData() {
    +247        if (studentNameParam == null || studentNameParam.trim().isEmpty()) {
    +248            JOptionPane.showMessageDialog(this, "Please select a student before submitting Abacus data.", "Missing student", JOptionPane.WARNING_MESSAGE);
    +249            return;
    +250        }
    +251
    +252        try {
    +253            int studentId = com.studentgui.apphelpers.Database.getOrCreateStudent(studentNameParam);
    +254            int ptId = com.studentgui.apphelpers.Database.getOrCreateProgressType("Abacus");
    +255            int sessionId = com.studentgui.apphelpers.Database.createProgressSession(studentId, ptId, dateParam);
    +256
    +257            String[] codes = new String[this.parts.length];
    +258            int[] scores = new int[this.parts.length];
    +259            for (int i = 0; i < this.parts.length; i++) {
    +260                codes[i] = this.parts[i][0];
    +261                scores[i] = skillFields[i].getValue();
    +262            }
    +263            com.studentgui.apphelpers.Database.insertAssessmentResults(sessionId, ptId, codes, scores);
    +264            LOG.info("Data submitted successfully via normalized schema.");
    +265            com.studentgui.apphelpers.UiNotifier.show("Abacus data saved.");
    +266            // Also persist this session as a JSON file in the student's folder (timestamped per-session)
    +267            com.studentgui.apphelpers.dto.AssessmentPayload payload = new com.studentgui.apphelpers.dto.AssessmentPayload(sessionId, codes, scores);
    +268            java.nio.file.Path jsonOut = com.studentgui.apphelpers.SessionJsonWriter.writeSessionJson(this.studentNameParam, "Abacus", payload, sessionId);
    +269            if (jsonOut == null) {
    +270                LOG.warn("Unable to save Abacus session JSON for sessionId={}", sessionId);
    +271            }
    +272            // Generate per-phase PNGs (time-series) and a markdown report for this session
    +273            try {
    +274                java.nio.file.Path plotsOut = com.studentgui.apphelpers.Helpers.studentPlotsDir(this.studentNameParam);
    +275                java.nio.file.Path reportsOut = com.studentgui.apphelpers.Helpers.studentReportsDir(this.studentNameParam);
    +276                java.nio.file.Files.createDirectories(plotsOut);
    +277                java.nio.file.Files.createDirectories(reportsOut);
    +278                java.time.format.DateTimeFormatter df = java.time.format.DateTimeFormatter.ISO_DATE;
    +279                String dateStr = (this.dateParam != null ? this.dateParam.format(df) : java.time.LocalDate.now().toString());
    +280                String baseName = "Abacus-" + sessionId + "-" + dateStr;
    +281
    +282                // Fetch recent dated sessions (oldest first) to build time-series plots.
    +283                com.studentgui.apphelpers.Database.ResultsWithDates rwd = com.studentgui.apphelpers.Database.fetchLatestAssessmentResultsWithDates(this.studentNameParam, "Abacus", Integer.MAX_VALUE);
    +284
    +285                java.util.Map<String, java.nio.file.Path> groups = null;
    +286                if (rwd != null && rwd.rows != null && !rwd.rows.isEmpty()) {
    +287                        // Build human-friendly labels from this.parts and render time-series grouped charts
    +288                        String[] labels = new String[this.parts.length];
    +289                        for (int i = 0; i < this.parts.length; i++) {
    +290                            labels[i] = this.parts[i][1];
    +291                        }
    +292                        lineGraph.updateWithGroupedDataByDate(rwd.dates, rwd.rows, codes, labels);
    +293                    // Persist each group as a PNG (time-series image)
    +294                    groups = lineGraph.saveGroupedCharts(plotsOut, baseName, 1000, 240);
    +295                    // Use the most-recent session date for the report header if available
    +296                    java.time.LocalDate headerDate = rwd.dates.get(rwd.dates.size() - 1);
    +297                    dateStr = headerDate.format(df);
    +298                } else {
    +299                    // Fallback: render only the latest session snapshot
    +300                    java.util.List<java.util.List<Integer>> rows = new java.util.ArrayList<>();
    +301                    java.util.List<Integer> latest = new java.util.ArrayList<>();
    +302                    for (int v : scores) {
    +303                        latest.add(v);
    +304                    }
    +305                    rows.add(latest);
    +306                    lineGraph.updateWithGroupedData(rows, codes);
    +307                    groups = lineGraph.saveGroupedCharts(plotsOut, baseName, 1000, 240);
    +308                }
    +309
    +310                // Generate markdown report
    +311                if (groups == null) {
    +312                    groups = new java.util.LinkedHashMap<>();
    +313                }
    +314                StringBuilder md = new StringBuilder();
    +315                md.append("# ").append(this.studentNameParam == null ? "Unknown Student" : this.studentNameParam).append(" - ").append(dateStr).append("\n\n");
    +316                for (java.util.Map.Entry<String, java.nio.file.Path> e : groups.entrySet()) {
    +317                    md.append("## ").append(e.getKey()).append("\n\n");
    +318                    md.append("![](./").append(e.getValue().getFileName().toString()).append(")\n\n");
    +319                }
    +320                java.nio.file.Path mdFile = reportsOut.resolve(baseName + ".md");
    +321                // images live in ../plots relative to reports
    +322                String mdText = md.toString().replace("![](./", "![](../plots/");
    +323                java.nio.file.Files.writeString(mdFile, mdText, java.nio.charset.StandardCharsets.UTF_8);
    +324                LOG.info("Wrote Abacus session report {} with {} group images", mdFile, groups.size());
    +325                // Also produce a simple HTML report that embeds the PNGs and
    +326                // shows a scrollable legend under each plot.
    +327                try {
    +328                    String[] palette = new String[] {"#1b9e77","#d95f02","#7570b3","#e7298a","#66a61e","#e6ab02","#a6761d","#666666"};
    +329
    +330                    // Build a map of group -> list of part indexes to recreate legend order
    +331                    java.util.LinkedHashMap<String, java.util.List<Integer>> groupsIdx = new java.util.LinkedHashMap<>();
    +332                    for (int i = 0; i < codes.length; i++) {
    +333                        String code = codes[i];
    +334                        String grp = code != null && code.contains("_") ? code.split("_")[0] : code;
    +335                        groupsIdx.computeIfAbsent(grp, k -> new java.util.ArrayList<>()).add(i);
    +336                    }
    +337
    +338                    StringBuilder html = new StringBuilder();
    +339                    html.append("<!doctype html>\n<html><head><meta charset=\"utf-8\"><title>");
    +340                    html.append(this.studentNameParam == null ? "Student Report" : this.studentNameParam).append(" - ").append(dateStr);
    +341                    html.append("</title>");
    +342                    html.append("<style>body{font-family:sans-serif;margin:20px;} img{max-width:100%;height:auto;border:1px solid #ccc;margin-bottom:8px;} .legend{max-height:160px;overflow:auto;border:1px solid #ddd;padding:8px;margin-bottom:24px;} .legend-item{display:flex;align-items:center;gap:8px;padding:4px 0;} .swatch{width:18px;height:12px;border:1px solid #333;display:inline-block}</style>");
    +343                    html.append("</head><body>");
    +344                    html.append("<h1>").append(this.studentNameParam == null ? "Unknown Student" : this.studentNameParam).append(" - ").append(dateStr).append("</h1>");
    +345
    +346                    for (java.util.Map.Entry<String, java.nio.file.Path> e2 : groups.entrySet()) {
    +347                        String grp = e2.getKey();
    +348                        String imgName = e2.getValue().getFileName().toString();
    +349                        html.append("<h2>").append(grp).append("</h2>");
    +350                        html.append("<div class=\"plot\"><img src=\"./").append(imgName).append("\" alt=\"").append(grp).append("\"></div>");
    +351
    +352                        // legend for this group
    +353                        java.util.List<Integer> idxs = groupsIdx.getOrDefault(grp, new java.util.ArrayList<>());
    +354                        html.append("<div class=\"legend\">");
    +355                        for (int s = 0; s < idxs.size(); s++) {
    +356                            int idx = idxs.get(s);
    +357                            String code = codes[idx];
    +358                            String human = this.parts[idx][1];
    +359                            String seriesName = code + " - " + human;
    +360                            String color = palette[s % palette.length];
    +361                            html.append("<div class=\"legend-item\"><span class=\"swatch\" style=\"background:" + color + ";\"></span>");
    +362                            html.append("<div>").append(seriesName).append("</div></div>");
    +363                        }
    +364                        html.append("</div>");
    +365                    }
    +366
    +367                    html.append("</body></html>");
    +368                    java.nio.file.Path htmlFile = reportsOut.resolve(baseName + ".html");
    +369                    // adjust image src to point to ../plots
    +370                    String htmlStr = html.toString().replace("src=\"./", "src=\"../plots/");
    +371                    java.nio.file.Files.writeString(htmlFile, htmlStr, java.nio.charset.StandardCharsets.UTF_8);
    +372                    LOG.info("Wrote Abacus HTML session report {}", htmlFile);
    +373                } catch (java.io.IOException ioex) {
    +374                    LOG.warn("Unable to write HTML report: {}", ioex.toString());
    +375                }
    +376            } catch (java.io.IOException | SQLException ex) {
    +377                LOG.warn("Unable to save Abacus per-phase plots or markdown report: {}", ex.toString());
    +378            }
    +379        } catch (SQLException e) {
    +380            LOG.error("SQL error in submitData", e);
    +381            JOptionPane.showMessageDialog(this, "Database error saving Abacus data: " + e.getMessage(), "Database error", JOptionPane.ERROR_MESSAGE);
    +382        }
    +383    }
    +384
    +385    /**
    +386     * Load recent assessment sessions for the selected student and update the
    +387     * shared {@link JLineGraph} with the returned metric series.
    +388     */
    +389    private void refreshGraph() {
    +390        try {
    +391            com.studentgui.apphelpers.Database.ResultsWithDates rwd = com.studentgui.apphelpers.Database.fetchLatestAssessmentResultsWithDates(studentNameParam, "Abacus", Integer.MAX_VALUE);
    +392            String[] codes = new String[this.parts.length];
    +393            for (int i = 0; i < this.parts.length; i++) {
    +394                codes[i] = this.parts[i][0];
    +395            }
    +396            if (rwd != null && rwd.rows != null && !rwd.rows.isEmpty()) {
    +397                // Use the date-aware grouped plotter so X axis is dates and each
    +398                // skill within a phase is a separate line series.
    +399                String[] labels = new String[this.parts.length];
    +400                for (int i = 0; i < this.parts.length; i++) {
    +401                    labels[i] = this.parts[i][1];
    +402                }
    +403                lineGraph.updateWithGroupedDataByDate(rwd.dates, rwd.rows, codes, labels);
    +404                LOG.debug("Graph updated with {} dated sessions", rwd.rows.size());
    +405            } else {
    +406                LOG.info("No data to plot; showing grouped placeholders.");
    +407                lineGraph.showEmptyGrouped(codes);
    +408            }
    +409        } catch (SQLException e) {
    +410            LOG.error("SQL error refreshing graph", e);
    +411        }
    +412    }
    +413    @Override
    +414    public void dateChanged(final LocalDate newDate) {
    +415        this.dateParam = newDate;
    +416        // When the global date changes, update the graph to reflect any
    +417        // date-related logic (most refreshGraph implementations load
    +418        // recent sessions independent of the selected session date, but
    +419        // updating here keeps the saved date in sync for future submits).
    +420        SwingUtilities.invokeLater(this::refreshGraph);
    +421    }
    +422
    +423    @Override
    +424    public void studentChanged(final String newStudent) {
    +425        this.studentNameParam = newStudent;
    +426        SwingUtilities.invokeLater(() -> {
    +427            refreshGraph();
    +428            updateTitleDate();
    +429        });
    +430    }
    +431
    +432        private void updateTitleDate() {
    +433            try {
    +434                String dateStr = this.dateParam != null ? this.dateParam.toString() : java.time.LocalDate.now().toString();
    +435                this.titleLabel.setText(baseTitle + " - " + dateStr);
    +436            } catch (Exception ex) {
    +437                this.titleLabel.setText(baseTitle);
    +438            }
    +439        }
    +440    
    +441
    +442}
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    + + diff --git a/target/site/apidocs/src-html/com/studentgui/apppages/Braille.html b/target/site/apidocs/src-html/com/studentgui/apppages/Braille.html new file mode 100644 index 0000000..fe86b9b --- /dev/null +++ b/target/site/apidocs/src-html/com/studentgui/apppages/Braille.html @@ -0,0 +1,533 @@ + + + + +Source code + + + + + + +
    +
    +
    001package com.studentgui.apppages;
    +002
    +003import java.awt.BorderLayout;
    +004import java.awt.Font;
    +005import java.awt.GridBagConstraints;
    +006import java.awt.GridBagLayout;
    +007import java.awt.Insets;
    +008import java.awt.event.ActionEvent;
    +009import java.awt.event.KeyEvent;
    +010import java.sql.SQLException;
    +011import java.time.LocalDate;
    +012import java.util.List;
    +013
    +014import javax.swing.JButton;
    +015import javax.swing.JLabel;
    +016import javax.swing.JOptionPane;
    +017import javax.swing.JPanel;
    +018import javax.swing.JScrollPane;
    +019import javax.swing.SwingUtilities;
    +020
    +021import org.slf4j.Logger;
    +022import org.slf4j.LoggerFactory;
    +023
    +024/**
    +025 * Braille skills progression assessment page.
    +026 *
    +027 * <p>Provides a comprehensive user interface for tracking student proficiency across
    +028 * 64 standardized Braille skills organized into 8 progressive phases following the
    +029 * Mangold Developmental Program sequence:</p>
    +030 *
    +031 * <ul>
    +032 *   <li><b>Phase 1 (P1_1–P1_4):</b> Foundational tracking and discrimination skills</li>
    +033 *   <li><b>Phase 2 (P2_1–P2_15):</b> Mangold letter progression (G C L → V J)</li>
    +034 *   <li><b>Phase 3 (P3_1–P3_15):</b> Contractions, wordsigns, and Grade 2 Braille basics</li>
    +035 *   <li><b>Phase 4 (P4_1–P4_4):</b> Indicators (Grade 1, capitals, numeric mode, typeform)</li>
    +036 *   <li><b>Phase 5 (P5_1–P5_4):</b> Document formatting (page numbers, headings, lists, poetry)</li>
    +037 *   <li><b>Phase 6 (P6_1–P6_7):</b> Basic Nemeth Math Code (operations, shapes, fractions)</li>
    +038 *   <li><b>Phase 7 (P7_1–P7_8):</b> Advanced Math (algebra, indices, radicals, functions, Greek)</li>
    +039 *   <li><b>Phase 8 (P8_1–P8_7):</b> Higher mathematics (modifiers, calculus, probability)</li>
    +040 * </ul>
    +041 *
    +042 * <p><b>Data Flow and Persistence:</b></p>
    +043 * <ul>
    +044 *   <li>Each skill is represented by a {@link com.studentgui.uicomp.PhaseScoreField} accepting integer scores (0–4 typical range)</li>
    +045 *   <li>On submission, values are persisted to the normalized schema via {@link com.studentgui.apphelpers.Database#insertAssessmentResults}</li>
    +046 *   <li>A timestamped JSON export is written to {@code StudentDataFiles/<student>/Sessions/Braille/}</li>
    +047 *   <li>Time-series plots are generated per phase group and saved as PNG images to {@code plots/}</li>
    +048 *   <li>Markdown and HTML reports are generated combining all phase plots with legend and metadata</li>
    +049 * </ul>
    +050 *
    +051 * <p><b>Generated Artifacts:</b></p>
    +052 * <ul>
    +053 *   <li><b>JSON session file:</b> {@code Braille-<sessionId>-<timestamp>.json}</li>
    +054 *   <li><b>Phase plots:</b> {@code Braille-<sessionId>-<date>-P<N>.png} (8 phase groups)</li>
    +055 *   <li><b>Markdown report:</b> {@code reports/Braille-<sessionId>-<date>.md}</li>
    +056 *   <li><b>HTML report:</b> {@code reports/Braille-<sessionId>-<date>.html} with embedded plots and color-coded legends</li>
    +057 * </ul>
    +058 *
    +059 * <p>The shared {@link JLineGraph} component visualizes recent session trends for the selected
    +060 * student, grouped by phase to prevent overcrowding. This page implements {@link com.studentgui.app.DateChangeListener}
    +061 * and {@link com.studentgui.app.StudentChangeListener} to refresh data when the global student or date selection changes.</p>
    +062 *
    +063 * @see com.studentgui.apphelpers.Database
    +064 * @see JLineGraph
    +065 * @see com.studentgui.uicomp.PhaseScoreField
    +066 */
    +067public class Braille extends JPanel implements com.studentgui.app.DateChangeListener, com.studentgui.app.StudentChangeListener {
    +068    private static final Logger LOG = LoggerFactory.getLogger(Braille.class);
    +069
    +070    /** Array of input components representing each Braille skill. */
    +071    private final com.studentgui.uicomp.PhaseScoreField[] skillFields;
    +072    /** Parts list for Braille (code,label) */
    +073    private final String[][] parts;
    +074    /** Flat list of part codes (derived from parts) */
    +075    private final String[] partCodes;
    +076    /** Shared graph used to plot recent results. */
    +077    private final JLineGraph lineGraph; // Reference to the JLineGraph instance
    +078    /** Selected student display name (may be null or placeholder). */
    +079    private String studentNameParam;
    +080    /** Session date used when creating progress sessions. */
    +081    private LocalDate dateParam;
    +082    /** Title label component displayed in the page header. */
    +083    private JLabel titleLabel;
    +084    /** Base title text for the Braille page; a date suffix may be appended for display. */
    +085    private final String baseTitle = "Braille Skills Progression";
    +086
    +087    /**
    +088     * Construct the Braille skills page for a given student and date.
    +089     *
    +090     * @param studentName the selected student name (may be null before selection)
    +091     * @param date the session date to use when creating a progress session
    +092     * @param lineGraph shared graph component used to display recent results
    +093     */
    +094    public Braille(final String studentName, final LocalDate date, final JLineGraph lineGraph) {
    +095        this.lineGraph = lineGraph; // Use the passed in graph instance
    +096    this.studentNameParam = (studentName == null || studentName.trim().isEmpty()) ? com.studentgui.apphelpers.Helpers.defaultStudent() : studentName;
    +097        this.dateParam = date != null ? date : LocalDate.now();
    +098        setLayout(new BorderLayout());
    +099
    +100        // Detailed Braille parts (code, visible label)
    +101        this.parts = new String[][]{
    +102            {"P1_1","1.1. Track left to right"},{"P1_2","1.2. Track top to bottom"},{"P1_3","1.3. Discriminate shapes"},{"P1_4","1.4. Discriminate braille characters"},
    +103            {"P2_1","2.1. Mangold Progression: G C L"},{"P2_2","2.2. Mangold Progression: D Y"},{"P2_3","2.3. Mangold Progression: A B"},{"P2_4","2.4. Mangold Progression: S"},
    +104            {"P2_5","2.5. Mangold Progression: W"},{"P2_6","2.6. Mangold Progression: P O"},{"P2_7","2.7. Mangold Progression: K"},{"P2_8","2.8. Mangold Progression: R"},
    +105            {"P2_9","2.9. Mangold Progression: M E"},{"P2_10","2.10. Mangold Progression: H"},{"P2_11","2.11. Mangold Progression: N X"},{"P2_12","2.12. Mangold Progression: Z F"},
    +106            {"P2_13","2.13. Mangold Progression: U T"},{"P2_14","2.14. Mangold Progression: Q I"},{"P2_15","2.15. Mangold Progression: V J"},
    +107            {"P3_1","3.1. Alphabetic Wordsigns"},{"P3_2","3.2. Braille Numbers"},{"P3_3","3.3. Punctuation"},{"P3_4","3.4. Strong Contractions (AND OF FOR WITH THE)"},
    +108            {"P3_5","3.5. Strong Groupsigns (CH GH SH TH WH ED ER OU OW ST AR ING)"},{"P3_6","3.6. Strong Wordsigns (CH SH TH WH OU ST)"},{"P3_7","3.7. Lower Groupsigns (BE CON DIS)"},
    +109            {"P3_8","3.8. Lower Groupsigns (EA BB CC FF GG)"},{"P3_9","3.9. Lower Groupsigns/Wordsigns (EN IN)"},{"P3_10","3.10. Lower Wordsigns (BE HIS WAS WERE)"},
    +110            {"P3_11","3.11. Dot 5 Contractions"},{"P3_12","3.12. Dot 45 Contractions"},{"P3_13","3.13. Dot 456 Contractions"},{"P3_14","3.14. Final Letter Groupsigns"},
    +111            {"P3_15","3.15. Shortform Words"},{"P4_1","4.1. Grade 1 Indicators"},{"P4_2","4.2. Capitals Indicators"},{"P4_3","4.3. Numeric Mode and Spatial math"},
    +112            {"P4_4","4.4. Typeform Indicators (ITALIC  SCRIPT  UNDERLINE  BOLDFACE)"},{"P5_1","5.1. Page Numbering"},{"P5_2","5.2. Headings"},{"P5_3","5.3. Lists"},
    +113            {"P5_4","5.4. Poety / Drama"},{"P6_1","6.1. Operation and Comparison Signs"},{"P6_2","6.2. Grade 1 Mode"},{"P6_3","6.3. Special Print Symbols"},
    +114            {"P6_4","6.4. Omission Marks"},{"P6_5","6.5. Shape Indicators"},{"P6_6","6.6. Roman Numerals"},{"P6_7","6.7. Fractions"},
    +115            {"P7_1","7.1. Grade 1 Mode and Algebra"},{"P7_2","7.2. Grade 1 Mode and Fractions"},{"P7_3","7.3. Advanced Operation and Comparison Signs"},{"P7_4","7.4. Indices"},
    +116            {"P7_5","7.5. Roots and Radicals"},{"P7_6","7.6. Miscellaneous Shape Indicators"},{"P7_7","7.7. Functions"},{"P7_8","7.8. Greek letters"},
    +117            {"P8_1","8.1. Functions"},{"P8_2","8.2. Modifiers  Bars  and Dots"},{"P8_3","8.3. Modifiers  Arrows  and Limits"},{"P8_4","8.4. Probability"},
    +118            {"P8_5","8.5. Calculus: Differentiation"},{"P8_6","8.6. Calculus: Integration"},{"P8_7","8.7. Vertical Bars"}
    +119        };
    +120        this.partCodes = new String[this.parts.length];
    +121        for (int i = 0; i < this.parts.length; i++) {
    +122            this.partCodes[i] = this.parts[i][0];
    +123        }
    +124
    +125        // Panel for data entry
    +126        JPanel dataEntryPanel = new JPanel();
    +127        dataEntryPanel.setLayout(new GridBagLayout());
    +128    JPanel view = new JPanel(new BorderLayout());
    +129    view.add(dataEntryPanel, BorderLayout.NORTH);
    +130    view.setBorder(javax.swing.BorderFactory.createEmptyBorder(20,20,20,20));
    +131    JScrollPane dataEntryScrollPane = new JScrollPane(view);
    +132    dataEntryScrollPane.setVerticalScrollBarPolicy(JScrollPane.VERTICAL_SCROLLBAR_AS_NEEDED);
    +133    dataEntryScrollPane.setHorizontalScrollBarPolicy(JScrollPane.HORIZONTAL_SCROLLBAR_NEVER);
    +134    dataEntryScrollPane.getAccessibleContext().setAccessibleName("Braille data entry scroll pane");
    +135
    +136    GridBagConstraints gbc = new GridBagConstraints();
    +137    gbc.insets = new Insets(2, 2, 2, 2);
    +138        gbc.fill = GridBagConstraints.HORIZONTAL;
    +139        gbc.weightx = 1.0;
    +140        gbc.weighty = 0.0;
    +141
    +142    this.titleLabel = new JLabel(baseTitle);
    +143        this.titleLabel.setFont(this.titleLabel.getFont().deriveFont(Font.BOLD, 16));
    +144        gbc.gridx = 0;
    +145        gbc.gridy = 0;
    +146        gbc.gridwidth = GridBagConstraints.REMAINDER;
    +147        dataEntryPanel.add(this.titleLabel, gbc);
    +148
    +149        gbc.gridy = 1;
    +150        gbc.gridwidth = GridBagConstraints.REMAINDER;
    +151        gbc.ipady = 20;
    +152        dataEntryPanel.add(new JPanel(), gbc);
    +153
    +154    // compute longest label width to align inputs
    +155        String[] labels = java.util.Arrays.stream(parts).map(x->x[1]).toArray(String[]::new);
    +156            int maxPx = com.studentgui.uicomp.PhaseScoreField.computeMaxLabelPixelWidth(titleLabel.getFont(), labels);
    +157            com.studentgui.uicomp.PhaseScoreField.setGlobalLabelWidth(Math.min(320, Math.max(140, maxPx + 50)));
    +158    skillFields = new com.studentgui.uicomp.PhaseScoreField[this.parts.length];
    +159        for (int i = 0; i < this.parts.length; i++) {
    +160            gbc.gridy = i + 2;
    +161            gbc.gridx = 0;
    +162            gbc.gridwidth = 1;
    +163            com.studentgui.uicomp.PhaseScoreField skillField = new com.studentgui.uicomp.PhaseScoreField(this.parts[i][1], 0);
    +164            skillField.setName("braille_" + this.parts[i][0]);
    +165            skillField.getAccessibleContext().setAccessibleName(this.parts[i][1]);
    +166            skillField.setToolTipText("Enter a numeric score for " + this.parts[i][1]);
    +167            gbc.gridx = 0; gbc.gridwidth = 2; gbc.insets = new Insets(2, 2, 2, 2);
    +168            dataEntryPanel.add(skillField, gbc);
    +169            skillFields[i] = skillField;
    +170            gbc.gridx = 2; gbc.insets = new Insets(2, 0, 2, 2);
    +171            dataEntryPanel.add(new JPanel(), gbc);
    +172        }
    +173
    +174    gbc.gridy = this.parts.length + 3;
    +175        gbc.gridx = 0;
    +176        gbc.gridwidth = GridBagConstraints.REMAINDER;
    +177        gbc.weighty = 1.0;
    +178        dataEntryPanel.add(new JPanel(), gbc);
    +179
    +180    // Place Submit and Open Latest side-by-side (match IOS/ScreenReader style)
    +181    gbc.gridy = this.parts.length + 4;
    +182    gbc.weighty = 0.0;
    +183    gbc.gridx = 0;
    +184    gbc.gridwidth = 1;
    +185    gbc.anchor = GridBagConstraints.WEST;
    +186    JButton submitDataButton = new JButton("Submit Data");
    +187    submitDataButton.setPreferredSize(new java.awt.Dimension(0, 32));
    +188    submitDataButton.addActionListener((ActionEvent e) -> { submitData(); refreshGraph(); });
    +189    submitDataButton.setMnemonic(KeyEvent.VK_S);
    +190    submitDataButton.setToolTipText("Save Braille scores for the selected student (Alt+S)");
    +191    submitDataButton.getAccessibleContext().setAccessibleName("Submit Braille Data");
    +192    dataEntryPanel.add(submitDataButton, gbc);
    +193
    +194    gbc.gridx = 1;
    +195    JButton openLatestBtn = new JButton("Open Latest Plot");
    +196    openLatestBtn.setPreferredSize(new java.awt.Dimension(0, 32));
    +197    openLatestBtn.addActionListener((ActionEvent e) -> {
    +198        java.nio.file.Path p = com.studentgui.apphelpers.Helpers.latestPlotPath(this.studentNameParam, "Braille");
    +199        if (p == null) {
    +200            com.studentgui.apphelpers.UiNotifier.show("No Braille plot found for student");
    +201        } else {
    +202            try {
    +203                java.awt.Desktop.getDesktop().open(p.toFile());
    +204            } catch (java.io.IOException | UnsupportedOperationException | SecurityException ex) {
    +205                com.studentgui.apphelpers.UiNotifier.show("Unable to open plot: " + p.getFileName().toString());
    +206            }
    +207        }
    +208    });
    +209    dataEntryPanel.add(openLatestBtn, gbc);
    +210
    +211    // consume remaining columns (if any) so layout stays compact
    +212    gbc.gridx = 2; gbc.gridwidth = GridBagConstraints.REMAINDER; gbc.anchor = GridBagConstraints.WEST;
    +213    dataEntryPanel.add(new JPanel(), gbc);
    +214
    +215        add(dataEntryScrollPane, BorderLayout.CENTER);
    +216
    +217        // Add existing graph reference
    +218        add(lineGraph, BorderLayout.SOUTH);
    +219
    +220        SwingUtilities.invokeLater(() -> {
    +221            dataEntryPanel.setPreferredSize(dataEntryPanel.getPreferredSize());
    +222            updateTitleDate();
    +223            revalidate();
    +224        });
    +225
    +226        com.studentgui.apphelpers.Helpers.createFolderHierarchy();
    +227        initDatabase();
    +228        refreshGraph();
    +229    }
    +230
    +231    /**
    +232     * Ensure the Braille progress-type and its assessment parts exist in the
    +233     * canonical schema. Safe to call repeatedly.
    +234     */
    +235    private void initDatabase() {
    +236        // Ensure normalized schema parts for Braille exist
    +237        try {
    +238            int ptId = com.studentgui.apphelpers.Database.getOrCreateProgressType("Braille");
    +239            // Use the canonical part codes defined in this.parts
    +240            com.studentgui.apphelpers.Database.ensureAssessmentParts(ptId, this.partCodes);
    +241        } catch (SQLException e) {
    +242            LOG.error("Error initializing Braille parts", e);
    +243        }
    +244    }
    +245
    +246    /**
    +247     * Read entered skill values and persist them as a new progress session.
    +248    * Performs integer validation and informs the user on invalid input.
    +249    *
    +250    * Implementation note: arrays used to call {@code insertAssessmentResults}
    +251    * are allocated dynamically based on the actual number of parts
    +252    * ({@code partCodes.length}) so that the stored columns exactly match the
    +253    * plotted series. This fixes a previous issue where fixed-size arrays
    +254    * could become out-of-sync with the parts list.
    +255     */
    +256    private void submitData() {
    +257        if (this.studentNameParam == null || this.studentNameParam.trim().isEmpty()) {
    +258            JOptionPane.showMessageDialog(this, "Please select a student before submitting Braille data.", "Missing student", JOptionPane.WARNING_MESSAGE);
    +259            return;
    +260        }
    +261
    +262        try {
    +263            int studentId = com.studentgui.apphelpers.Database.getOrCreateStudent(this.studentNameParam);
    +264            int ptId = com.studentgui.apphelpers.Database.getOrCreateProgressType("Braille");
    +265            int sessionId = com.studentgui.apphelpers.Database.createProgressSession(studentId, ptId, this.dateParam);
    +266
    +267            // Allocate arrays based on the actual number of parts so that
    +268            // the submitted data and plotted series stay in sync.
    +269            String[] codes = new String[this.partCodes.length];
    +270            int[] scores = new int[this.partCodes.length];
    +271            for (int i = 0; i < this.partCodes.length; i++) {
    +272                codes[i] = this.partCodes[i];
    +273                scores[i] = skillFields[i].getValue();
    +274            }
    +275            com.studentgui.apphelpers.Database.insertAssessmentResults(sessionId, ptId, codes, scores);
    +276            LOG.info("Data submitted successfully via normalized schema.");
    +277            com.studentgui.apphelpers.UiNotifier.show("Braille data saved.");
    +278            com.studentgui.apphelpers.dto.AssessmentPayload payload = new com.studentgui.apphelpers.dto.AssessmentPayload(sessionId, codes, scores);
    +279            java.nio.file.Path jsonOut = com.studentgui.apphelpers.SessionJsonWriter.writeSessionJson(this.studentNameParam, "Braille", payload, sessionId);
    +280            if (jsonOut == null) {
    +281                LOG.warn("Unable to save Braille session JSON for sessionId={}", sessionId);
    +282            }
    +283            try {
    +284                java.nio.file.Path plotsOut = com.studentgui.apphelpers.Helpers.studentPlotsDir(this.studentNameParam);
    +285                java.nio.file.Path reportsOut = com.studentgui.apphelpers.Helpers.studentReportsDir(this.studentNameParam);
    +286                java.nio.file.Files.createDirectories(plotsOut);
    +287                java.nio.file.Files.createDirectories(reportsOut);
    +288                java.time.format.DateTimeFormatter df = java.time.format.DateTimeFormatter.ISO_DATE;
    +289                String dateStr = this.dateParam != null ? this.dateParam.format(df) : java.time.LocalDate.now().toString();
    +290                String baseName = "Braille-" + sessionId + "-" + dateStr;
    +291
    +292                com.studentgui.apphelpers.Database.ResultsWithDates rwd = com.studentgui.apphelpers.Database.fetchLatestAssessmentResultsWithDates(this.studentNameParam, "Braille", Integer.MAX_VALUE);
    +293                java.util.Map<String, java.nio.file.Path> groups = null;
    +294                String[] labels = new String[this.parts.length];
    +295                for (int i = 0; i < this.parts.length; i++) {
    +296                    labels[i] = this.parts[i][1];
    +297                }
    +298                if (rwd != null && rwd.rows != null && !rwd.rows.isEmpty()) {
    +299                    lineGraph.updateWithGroupedDataByDate(rwd.dates, rwd.rows, this.partCodes, labels);
    +300                    groups = lineGraph.saveGroupedCharts(plotsOut, baseName, 1000, 240);
    +301                    java.time.LocalDate headerDate = rwd.dates.get(rwd.dates.size() - 1);
    +302                    dateStr = headerDate.format(df);
    +303                } else {
    +304                    java.util.List<java.util.List<Integer>> rowsList = new java.util.ArrayList<>();
    +305                    java.util.List<Integer> latest = new java.util.ArrayList<>();
    +306                    for (int v : scores) {
    +307                        latest.add(v);
    +308                    }
    +309                    rowsList.add(latest);
    +310                    lineGraph.updateWithGroupedData(rowsList, this.partCodes);
    +311                    groups = lineGraph.saveGroupedCharts(plotsOut, baseName, 1000, 240);
    +312                }
    +313
    +314                if (groups == null) {
    +315                    groups = new java.util.LinkedHashMap<>();
    +316                }
    +317                StringBuilder md = new StringBuilder();
    +318                md.append("# ").append(this.studentNameParam == null ? "Unknown Student" : this.studentNameParam).append(" - ").append(dateStr).append("\n\n");
    +319                for (java.util.Map.Entry<String, java.nio.file.Path> e : groups.entrySet()) {
    +320                    md.append("## ").append(e.getKey()).append("\n\n");
    +321                    md.append("![](./").append(e.getValue().getFileName().toString()).append(")\n\n");
    +322                }
    +323                java.nio.file.Path mdFile = reportsOut.resolve(baseName + ".md");
    +324                // images live in ../plots relative to reports
    +325                String mdText = md.toString().replace("![](./", "![](../plots/");
    +326                java.nio.file.Files.writeString(mdFile, mdText, java.nio.charset.StandardCharsets.UTF_8);
    +327
    +328                // HTML report using shared palette
    +329                try {
    +330                    String[] palette = JLineGraph.PALETTE_HEX;
    +331                    java.util.LinkedHashMap<String, java.util.List<Integer>> groupsIdx = new java.util.LinkedHashMap<>();
    +332                    for (int i = 0; i < this.partCodes.length; i++) {
    +333                        String code = this.partCodes[i];
    +334                        String grp = code != null && code.contains("_") ? code.split("_")[0] : code;
    +335                        groupsIdx.computeIfAbsent(grp, k -> new java.util.ArrayList<>()).add(i);
    +336                    }
    +337                    StringBuilder html = new StringBuilder();
    +338                    html.append("<!doctype html><html><head><meta charset=\"utf-8\"><title>");
    +339                    html.append(this.studentNameParam == null ? "Student Report" : this.studentNameParam).append(" - ").append(dateStr).append("</title>");
    +340                    html.append("<style>body{font-family:sans-serif;margin:20px;} img{max-width:100%;height:auto;border:1px solid #ccc;margin-bottom:8px;} .legend{max-height:160px;overflow:auto;border:1px solid #ddd;padding:8px;margin-bottom:24px;} .legend-item{display:flex;align-items:center;gap:8px;padding:4px 0;} .swatch{width:18px;height:12px;border:1px solid #333;display:inline-block}</style>");
    +341                    html.append("</head><body>");
    +342                    html.append("<h1>").append(this.studentNameParam == null ? "Unknown Student" : this.studentNameParam).append(" - ").append(dateStr).append("</h1>");
    +343                    for (java.util.Map.Entry<String, java.nio.file.Path> e2 : groups.entrySet()) {
    +344                        String grp = e2.getKey();
    +345                        String imgName = e2.getValue().getFileName().toString();
    +346                        html.append("<h2>").append(grp).append("</h2>");
    +347                        html.append("<div class=\"plot\"><img src=\"./").append(imgName).append("\" alt=\"").append(grp).append("\"></div>");
    +348                        java.util.List<Integer> idxs = groupsIdx.getOrDefault(grp, new java.util.ArrayList<>());
    +349                        html.append("<div class=\"legend\">");
    +350                        for (int s = 0; s < idxs.size(); s++) {
    +351                            int idx = idxs.get(s);
    +352                            String code = this.partCodes[idx];
    +353                            String human = this.parts[idx][1];
    +354                            String seriesName = code + " - " + human;
    +355                            String color = palette[s % palette.length];
    +356                            html.append("<div class=\"legend-item\">");
    +357                            html.append("<span class=\"swatch\" style=\"background:");
    +358                            html.append(color);
    +359                            html.append(";\"></span>");
    +360                            html.append("<div>");
    +361                            html.append(seriesName);
    +362                            html.append("</div></div>");
    +363                        }
    +364                        html.append("</div>");
    +365                    }
    +366                    html.append("</body></html>");
    +367                    java.nio.file.Path htmlFile = reportsOut.resolve(baseName + ".html");
    +368                    String htmlStr = html.toString().replace("src=\"./", "src=\"../plots/");
    +369                    java.nio.file.Files.writeString(htmlFile, htmlStr, java.nio.charset.StandardCharsets.UTF_8);
    +370                    LOG.info("Wrote Braille HTML session report {}", htmlFile);
    +371                } catch (java.io.IOException ioex) {
    +372                    LOG.warn("Unable to write Braille HTML report: {}", ioex.toString());
    +373                }
    +374
    +375                LOG.info("Wrote Braille session report {} with {} group images", mdFile, groups.size());
    +376            } catch (java.io.IOException | SQLException ex) {
    +377                LOG.warn("Unable to save Braille per-phase plots or markdown report: {}", ex.toString());
    +378            }
    +379        } catch (SQLException e) {
    +380            LOG.error("Unexpected error submitting braille data", e);
    +381            JOptionPane.showMessageDialog(this, "Database error saving Braille data: " + e.getMessage(), "Database error", JOptionPane.ERROR_MESSAGE);
    +382        }
    +383    }
    +384    /**
    +385     * Fetch recent assessment sessions and update the shared graph view.
    +386     */
    +387    private void refreshGraph() {
    +388        try {
    +389            List<List<Integer>> allSkillValues = com.studentgui.apphelpers.Database.fetchLatestAssessmentResults(this.studentNameParam, "Braille", 5);
    +390            // Note: pages should supply the selected student name; here the existing code used a passed-in studentName variable
    +391            // We will try to use the first skill field's content as a student name fallback; in the UI flow this should be provided.
    +392            // For now use a placeholder when no student is selected.
    +393            if (allSkillValues != null && !allSkillValues.isEmpty()) {
    +394                lineGraph.updateWithGroupedData(allSkillValues, this.partCodes);
    +395                // Write to the consolidated per-run data dumps file when enabled
    +396                if (Boolean.parseBoolean(com.studentgui.apphelpers.Settings.get("dump.enabled", "false"))) {
    +397                    try {
    +398                        String appHome = System.getProperty("APP_HOME", com.studentgui.apphelpers.Helpers.APP_HOME.toString());
    +399                        String ts = System.getProperty("LOG_TS", String.valueOf(java.time.Instant.now().getEpochSecond()));
    +400                        java.nio.file.Path logDir = java.nio.file.Paths.get(appHome).resolve("logs");
    +401                        java.nio.file.Files.createDirectories(logDir);
    +402                        java.nio.file.Path logFile = logDir.resolve("data_dumps_" + ts + ".log");
    +403                        StringBuilder sb = new StringBuilder();
    +404                        java.time.format.DateTimeFormatter dtf = java.time.format.DateTimeFormatter.ISO_DATE_TIME;
    +405                        sb.append("[Braille]").append(System.lineSeparator());
    +406                        sb.append(java.time.LocalDateTime.now().format(dtf)).append(" - student=").append(this.studentNameParam).append(System.lineSeparator());
    +407                        sb.append("data=").append(allSkillValues.toString()).append(System.lineSeparator());
    +408                        sb.append(System.lineSeparator());
    +409                        java.nio.file.Files.writeString(logFile, sb.toString(), java.nio.charset.StandardCharsets.UTF_8, java.nio.file.StandardOpenOption.CREATE, java.nio.file.StandardOpenOption.APPEND);
    +410                    } catch (java.io.IOException ioe) {
    +411                        LOG.trace("Unable to write braille load log: {}", ioe.toString());
    +412                    }
    +413                }
    +414            } else {
    +415                LOG.info("No data to plot; showing grouped placeholders.");
    +416                lineGraph.showEmptyGrouped(this.partCodes);
    +417            }
    +418        } catch (SQLException e) {
    +419            LOG.error("SQL error refreshing braille graph", e);
    +420        }
    +421    }
    +422
    +423    @Override
    +424    public void dateChanged(final LocalDate newDate) {
    +425        this.dateParam = newDate;
    +426        SwingUtilities.invokeLater(() -> {
    +427            refreshGraph();
    +428            updateTitleDate();
    +429        });
    +430    }
    +431    
    +432    @Override
    +433    public void studentChanged(final String newStudent) {
    +434        this.studentNameParam = newStudent != null ? newStudent : "Unknown Student";
    +435        SwingUtilities.invokeLater(() -> {
    +436            refreshGraph();
    +437            updateTitleDate();
    +438        });
    +439    }
    +440    
    +441    /**
    +442     * Update the page title label to include the current session date.
    +443     * Falls back to base title if date formatting fails.
    +444     */
    +445    private void updateTitleDate() {
    +446        try {
    +447            String dateStr = this.dateParam != null ? this.dateParam.toString() : java.time.LocalDate.now().toString();
    +448            this.titleLabel.setText(baseTitle + " - " + dateStr);
    +449        } catch (Exception ex) {
    +450            this.titleLabel.setText(baseTitle);
    +451        }
    +452    }
    +453    
    +454
    +455}
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    + + diff --git a/target/site/apidocs/src-html/com/studentgui/apppages/BrailleNote.html b/target/site/apidocs/src-html/com/studentgui/apppages/BrailleNote.html new file mode 100644 index 0000000..2094430 --- /dev/null +++ b/target/site/apidocs/src-html/com/studentgui/apppages/BrailleNote.html @@ -0,0 +1,536 @@ + + + + +Source code + + + + + + +
    +
    +
    001package com.studentgui.apppages;
    +002import java.awt.BorderLayout;
    +003import java.awt.Font; 
    +004import java.awt.GridBagConstraints;
    +005import java.awt.GridBagLayout;
    +006import java.awt.Insets;
    +007import java.awt.event.ActionEvent;
    +008import java.awt.event.KeyEvent;
    +009import java.sql.SQLException;
    +010import java.time.LocalDate;
    +011import java.util.List;
    +012
    +013import javax.swing.JButton;
    +014import javax.swing.JLabel;
    +015import javax.swing.JOptionPane;
    +016import javax.swing.JPanel;
    +017import javax.swing.JScrollPane;
    +018import javax.swing.SwingUtilities;
    +019
    +020import org.slf4j.Logger;
    +021import org.slf4j.LoggerFactory;
    +022
    +023/**
    +024 * HumanWare BrailleNote Touch Plus (BNT+) proficiency assessment page.
    +025 *
    +026 * <p>Evaluates student competency with the BrailleNote Touch Plus refreshable braille notetaker
    +027 * and productivity device across 52 skills organized into 12 functional domains:</p>
    +028 *
    +029 * <ul>
    +030 *   <li><b>Phase 1 (P1_1–P1_9): Device Fundamentals and Core Applications</b>
    +031 *     <ul>
    +032 *       <li>Physical layout (braille keyboard, navigation keys, touchscreen, ports)</li>
    +033 *       <li>Setup procedures and universal commands (power, mode switching, context menus)</li>
    +034 *       <li>BNT+ navigation paradigm (gestures, quick keys, braille commands)</li>
    +035 *       <li>File management (folders, copy/paste, rename, delete)</li>
    +036 *       <li>Word processor (KeyWord): document creation, editing, formatting</li>
    +037 *       <li>Email (KeyMail): compose, send, receive, attachments</li>
    +038 *       <li>Internet browsing (KeyWeb): navigation, bookmarks, forms</li>
    +039 *       <li>Calculator and KeyMath (arithmetic, scientific functions)</li>
    +040 *     </ul>
    +041 *   </li>
    +042 *   <li><b>Phase 2 (P2_1–P2_7): Productivity Suite Applications</b>
    +043 *     <ul>
    +044 *       <li>Calendar management (appointments, reminders, recurring events)</li>
    +045 *       <li>KeyBRF (Braille file viewer/editor)</li>
    +046 *       <li>KeyFiles (file explorer and organizer)</li>
    +047 *       <li>KeyMail (advanced email features)</li>
    +048 *       <li>KeyWeb (advanced browsing, accessibility modes)</li>
    +049 *       <li>KeyCalc (spreadsheet concepts)</li>
    +050 *       <li>KeyWord (advanced formatting, styles, tables)</li>
    +051 *     </ul>
    +052 *   </li>
    +053 *   <li><b>Phase 3 (P3_1–P3_7): Advanced Applications and Accessibility</b>
    +054 *     <ul>
    +055 *       <li>KeySlides (presentation creation and delivery)</li>
    +056 *       <li>KeyCode (text editor with syntax highlighting for programming)</li>
    +057 *       <li>Third-party app integration (Dropbox, Google Drive, OneDrive)</li>
    +058 *       <li>Braille input configuration (computer braille, contracted, literary)</li>
    +059 *       <li>Braille output settings (display mode, translation tables)</li>
    +060 *       <li>Device settings and preferences</li>
    +061 *       <li>Accessibility features (speech output, magnification, contrast)</li>
    +062 *     </ul>
    +063 *   </li>
    +064 *   <li><b>Phase 4 (P4_1–P4_3): Advanced File and Cloud Management</b></li>
    +065 *   <li><b>Phase 5 (P5_1–P5_4): Collaboration and Export Workflows</b></li>
    +066 *   <li><b>Phase 6 (P6_1–P6_3): App Ecosystem and Troubleshooting</b></li>
    +067 *   <li><b>Phase 7 (P7_1–P7_4): Automation and Customization</b></li>
    +068 *   <li><b>Phase 8 (P8_1–P8_5): Peripheral Integration</b> (Bluetooth/USB devices, displays, audio/video)</li>
    +069 *   <li><b>Phase 9 (P9_1–P9_4): Security and Network Configuration</b></li>
    +070 *   <li><b>Phase 10 (P10_1–P10_3): Speech Engine Customization</b></li>
    +071 *   <li><b>Phase 11 (P11_1–P11_5): Maintenance and Support</b> (firmware, diagnostics, warranty)</li>
    +072 *   <li><b>Phase 12 (P12_1–P12_4): Community and Online Resources</b></li>
    +073 * </ul>
    +074 *
    +075 * <p><b>Data Management and Artifacts:</b></p>
    +076 * <ul>
    +077 *   <li>Scores captured via {@link com.studentgui.uicomp.PhaseScoreField} (integer 0–4 typical)</li>
    +078 *   <li>Persisted to normalized schema via {@link com.studentgui.apphelpers.Database#insertAssessmentResults}</li>
    +079 *   <li>JSON export: {@code StudentDataFiles/<student>/Sessions/BrailleNote/BrailleNote-<sessionId>-<timestamp>.json}</li>
    +080 *   <li>Phase-grouped time-series plots: {@code plots/BrailleNote-<sessionId>-<date>-P<N>.png} (12 phase groups)</li>
    +081 *   <li>Markdown and HTML reports with embedded plots and color-coded legends</li>
    +082 * </ul>
    +083 *
    +084 * <p>The shared {@link JLineGraph} visualizes recent session trends grouped by phase prefix.
    +085 * Implements {@link com.studentgui.app.DateChangeListener} and {@link com.studentgui.app.StudentChangeListener}
    +086 * for dynamic updates when global student/date selections change.</p>
    +087 *
    +088 * @see com.studentgui.apphelpers.Database
    +089 * @see JLineGraph
    +090 * @see com.studentgui.uicomp.PhaseScoreField
    +091 */
    +092public class BrailleNote extends JPanel implements com.studentgui.app.DateChangeListener, com.studentgui.app.StudentChangeListener {
    +093    private static final Logger LOG = LoggerFactory.getLogger(BrailleNote.class);
    +094
    +095    /** Inputs for each BrailleNote skill. */
    +096    private final com.studentgui.uicomp.PhaseScoreField[] skillFields;
    +097    /** Canonical assessment part codes and labels for BrailleNote. */
    +098    private final String[][] parts;
    +099    /** Shared graph component for plotting results. */
    +100    private final JLineGraph lineGraph; // Reference to the JLineGraph instance
    +101    /** Display name of the selected student (may be null). */
    +102    private String studentNameParam;
    +103    /** Header title label for this page. */
    +104    private JLabel titleLabel;
    +105    /** Base page title string used when rendering the header (date appended). */
    +106    private final String baseTitle = "BrailleNote Skills Progression";
    +107    /** Session date associated with persisted progress. */
    +108    private LocalDate dateParam;
    +109
    +110    /**
    +111     * Create the BrailleNote page for a specific student and date.
    +112     *
    +113     * @param studentName the selected student name (may be null until a student is chosen)
    +114     * @param date the date for the session (used when creating a progress session)
    +115     * @param lineGraph shared graph component used to display recent results
    +116     */
    +117    public BrailleNote(String studentName, LocalDate date, JLineGraph lineGraph) {
    +118    this.studentNameParam = (studentName == null || studentName.trim().isEmpty()) ? com.studentgui.apphelpers.Helpers.defaultStudent() : studentName;
    +119        this.dateParam = date;
    +120        this.lineGraph = lineGraph; // Use the passed in graph instance
    +121        setLayout(new BorderLayout());
    +122
    +123    this.parts = new String[][]{
    +124            {"P1_1","1.1 Physical Layout"},{"P1_2","1.2 Setup/Universal Commands"},{"P1_3","1.3 BNT+ Navigation"},{"P1_4","1.4 File Management"},{"P1_5","1.5 Word Processor"},{"P1_6","1.6 Email"},{"P1_7","1.7 Internet"},{"P1_8","1.8 Calculator"},{"P1_9","1.9 KeyMath"},
    +125            {"P2_1","2.1 Calendar"},{"P2_2","2.2 KeyBRF"},{"P2_3","2.3 KeyFiles"},{"P2_4","2.4 KeyMail"},{"P2_5","2.5 KeyWeb"},{"P2_6","2.6 KeyCalc"},{"P2_7","2.7 KeyWord"},
    +126            {"P3_1","3.1 KeySlides"},{"P3_2","3.2 KeyCode"},{"P3_3","3.3 Third Party Apps"},{"P3_4","3.4 Braille Input"},{"P3_5","3.5 Braille Output"},{"P3_6","3.6 Settings"},{"P3_7","3.7 Accessibility"},
    +127            {"P4_1","4.1 Advanced File Management"},{"P4_2","4.2 Cloud Integration"},{"P4_3","4.3 Device Maintenance"},
    +128            {"P5_1","5.1 Collaboration"},{"P5_2","5.2 Export/Import"},{"P5_3","5.3 Printing"},{"P5_4","5.4 Backup"},
    +129            {"P6_1","6.1 App Installation"},{"P6_2","6.2 App Updates"},{"P6_3","6.3 Troubleshooting"},
    +130            {"P7_1","7.1 Custom Shortcuts"},{"P7_2","7.2 Macros"},{"P7_3","7.3 Scripting"},{"P7_4","7.4 Automation"},
    +131            {"P8_1","8.1 Bluetooth Devices"},{"P8_2","8.2 USB Devices"},{"P8_3","8.3 External Displays"},{"P8_4","8.4 Audio Output"},{"P8_5","8.5 Video Output"},
    +132            {"P9_1","9.1 Security"},{"P9_2","9.2 User Accounts"},{"P9_3","9.3 Parental Controls"},{"P9_4","9.4 Network Settings"},
    +133            {"P10_1","10.1 Speech Settings"},{"P10_2","10.2 Voice Profiles"},{"P10_3","10.3 Language Support"},
    +134            {"P11_1","11.1 Firmware Updates"},{"P11_2","11.2 Diagnostics"},{"P11_3","11.3 Logs"},{"P11_4","11.4 Support"},{"P11_5","11.5 Warranty"},
    +135            {"P12_1","12.1 Community Resources"},{"P12_2","12.2 Online Help"},{"P12_3","12.3 User Forums"},{"P12_4","12.4 Feedback"}
    +136        };
    +137
    +138        // Panel for data entry
    +139        JPanel dataEntryPanel = new JPanel();
    +140        dataEntryPanel.setLayout(new GridBagLayout());
    +141    JScrollPane dataEntryScrollPane = new JScrollPane(dataEntryPanel);
    +142    dataEntryScrollPane.setVerticalScrollBarPolicy(JScrollPane.VERTICAL_SCROLLBAR_AS_NEEDED);
    +143    dataEntryScrollPane.setHorizontalScrollBarPolicy(JScrollPane.HORIZONTAL_SCROLLBAR_NEVER);
    +144    dataEntryScrollPane.getAccessibleContext().setAccessibleName("BrailleNote data entry scroll pane");
    +145
    +146        GridBagConstraints gbc = new GridBagConstraints();
    +147        gbc.insets = new Insets(5, 5, 5, 5);
    +148        gbc.fill = GridBagConstraints.HORIZONTAL;
    +149        gbc.weightx = 1.0;
    +150        gbc.weighty = 0.0;
    +151
    +152    this.titleLabel = new JLabel(baseTitle);
    +153        this.titleLabel.setFont(this.titleLabel.getFont().deriveFont(Font.BOLD, 16));
    +154        gbc.gridx = 0;
    +155        gbc.gridy = 0;
    +156        gbc.gridwidth = GridBagConstraints.REMAINDER;
    +157        dataEntryPanel.add(this.titleLabel, gbc);
    +158
    +159        gbc.gridy = 1;
    +160        gbc.gridwidth = GridBagConstraints.REMAINDER;
    +161        gbc.ipady = 20;
    +162        dataEntryPanel.add(new JPanel(), gbc);
    +163
    +164    // layout spacing handled by PhaseScoreField
    +165
    +166    // compute pixel width using font metrics so labels align precisely
    +167    String[] labelsArr = java.util.Arrays.stream(parts).map(x->x[1]).toArray(String[]::new);
    +168    int maxPx = com.studentgui.uicomp.PhaseScoreField.computeMaxLabelPixelWidth(titleLabel.getFont(), labelsArr);
    +169    com.studentgui.uicomp.PhaseScoreField.setGlobalLabelWidth(Math.min(320, Math.max(140, maxPx + 50)));
    +170    skillFields = new com.studentgui.uicomp.PhaseScoreField[parts.length];
    +171        for (int i = 0; i < parts.length; i++) {
    +172            gbc.gridy = i + 2;
    +173            gbc.gridx = 0;
    +174            gbc.gridwidth = 1;
    +175            com.studentgui.uicomp.PhaseScoreField field = new com.studentgui.uicomp.PhaseScoreField(parts[i][1], 0);
    +176            field.setName("braillenote_" + parts[i][0]);
    +177            field.getAccessibleContext().setAccessibleName(parts[i][1]);
    +178            field.setToolTipText("Enter a numeric score for " + parts[i][1]);
    +179            gbc.gridx = 0; gbc.gridwidth = 2; gbc.insets = new Insets(5, 5, 5, 5);
    +180            dataEntryPanel.add(field, gbc);
    +181            skillFields[i] = field;
    +182            gbc.gridx = 2; gbc.gridwidth = 1; gbc.insets = new Insets(5, 0, 5, 5);
    +183            dataEntryPanel.add(new JPanel(), gbc);
    +184        }
    +185
    +186    gbc.gridy = parts.length + 3;
    +187        gbc.gridx = 0;
    +188        gbc.gridwidth = GridBagConstraints.REMAINDER;
    +189        gbc.weighty = 1.0;
    +190        dataEntryPanel.add(new JPanel(), gbc);
    +191
    +192    gbc.gridy = parts.length + 4;
    +193        gbc.weighty = 0.0;
    +194        // layout spacing handled by PhaseScoreField
    +195    // Place Submit and Open Latest side-by-side like IOS/ScreenReader
    +196    gbc.gridy = parts.length + 4; gbc.gridx = 0; gbc.gridwidth = 1; gbc.anchor = GridBagConstraints.WEST;
    +197    JButton submitDataButton = new JButton("Submit Data");
    +198    submitDataButton.setPreferredSize(new java.awt.Dimension(0, 32));
    +199    submitDataButton.addActionListener((ActionEvent e) -> { submitData(); refreshGraph(); });
    +200    submitDataButton.setMnemonic(KeyEvent.VK_S);
    +201    submitDataButton.setToolTipText("Save BrailleNote scores for the selected student (Alt+S)");
    +202    submitDataButton.getAccessibleContext().setAccessibleName("Submit BrailleNote Data");
    +203    dataEntryPanel.add(submitDataButton, gbc);
    +204
    +205    gbc.gridx = 1; gbc.gridwidth = 1; gbc.anchor = GridBagConstraints.WEST;
    +206    JButton openLatestBtn = new JButton("Open Latest Plot");
    +207    openLatestBtn.setPreferredSize(new java.awt.Dimension(0, 32));
    +208    openLatestBtn.addActionListener((ActionEvent e) -> {
    +209        java.nio.file.Path p = com.studentgui.apphelpers.Helpers.latestPlotPath(this.studentNameParam, "BrailleNote");
    +210        if (p == null) {
    +211            com.studentgui.apphelpers.UiNotifier.show("No BrailleNote plot found for student");
    +212        } else {
    +213            try {
    +214                java.awt.Desktop.getDesktop().open(p.toFile());
    +215            } catch (java.io.IOException | UnsupportedOperationException | SecurityException ex) {
    +216                com.studentgui.apphelpers.UiNotifier.show("Unable to open plot: " + p.getFileName().toString());
    +217            }
    +218        }
    +219    });
    +220    dataEntryPanel.add(openLatestBtn, gbc);
    +221
    +222        add(dataEntryScrollPane, BorderLayout.CENTER);
    +223
    +224        // Add existing graph reference
    +225        add(lineGraph, BorderLayout.SOUTH);
    +226
    +227        SwingUtilities.invokeLater(() -> {
    +228            dataEntryPanel.setPreferredSize(dataEntryPanel.getPreferredSize());
    +229            updateTitleDate();
    +230            revalidate();
    +231        });
    +232
    +233        // Ensure application folders and DB schema exist
    +234        com.studentgui.apphelpers.Helpers.createFolderHierarchy();
    +235        initDatabase();
    +236        refreshGraph();
    +237    }
    +238
    +239    /**
    +240     * Ensure the progress-type and assessment part rows for BrailleNote exist
    +241     * in the normalized schema. This is safe to call repeatedly.
    +242     */
    +243    private void initDatabase() {
    +244        try {
    +245            int ptId = com.studentgui.apphelpers.Database.getOrCreateProgressType("BrailleNote");
    +246            String[] codes = new String[this.parts.length];
    +247            for (int i = 0; i < this.parts.length; i++) {
    +248                codes[i] = this.parts[i][0];
    +249            }
    +250            com.studentgui.apphelpers.Database.ensureAssessmentParts(ptId, codes);
    +251        } catch (SQLException e) {
    +252            LOG.error("SQL error initializing braille note parts", e);
    +253        }
    +254    }
    +255
    +256    /**
    +257     * Read the values entered into the skill fields and persist them to the
    +258     * database as a new progress session. Validation is performed to ensure
    +259     * numeric integer input; users are prompted on invalid values.
    +260     */
    +261    private void submitData() {
    +262        if (this.studentNameParam == null || this.studentNameParam.trim().isEmpty()) {
    +263            JOptionPane.showMessageDialog(this, "Please select a student before submitting BrailleNote data.", "Missing student", JOptionPane.WARNING_MESSAGE);
    +264            return;
    +265        }
    +266
    +267        try {
    +268            int studentId = com.studentgui.apphelpers.Database.getOrCreateStudent(this.studentNameParam);
    +269            int ptId = com.studentgui.apphelpers.Database.getOrCreateProgressType("BrailleNote");
    +270            int sessionId = com.studentgui.apphelpers.Database.createProgressSession(studentId, ptId, this.dateParam);
    +271            String[] codes = new String[parts.length];
    +272            int[] scores = new int[parts.length];
    +273            for (int i = 0; i < parts.length && i < skillFields.length; i++) {
    +274                codes[i] = parts[i][0];
    +275                scores[i] = skillFields[i].getValue();
    +276            }
    +277            com.studentgui.apphelpers.Database.insertAssessmentResults(sessionId, ptId, codes, scores);
    +278            LOG.info("Data submitted successfully via normalized schema.");
    +279            com.studentgui.apphelpers.UiNotifier.show("BrailleNote data saved.");
    +280            com.studentgui.apphelpers.dto.AssessmentPayload payload = new com.studentgui.apphelpers.dto.AssessmentPayload(sessionId, codes, scores);
    +281            java.nio.file.Path jsonOut = com.studentgui.apphelpers.SessionJsonWriter.writeSessionJson(this.studentNameParam, "BrailleNote", payload, sessionId);
    +282            if (jsonOut == null) {
    +283                LOG.warn("Unable to save BrailleNote session JSON for sessionId={}", sessionId);
    +284            }
    +285            try {
    +286                java.nio.file.Path plotsOut = com.studentgui.apphelpers.Helpers.studentPlotsDir(this.studentNameParam);
    +287                java.nio.file.Path reportsOut = com.studentgui.apphelpers.Helpers.studentReportsDir(this.studentNameParam);
    +288                java.nio.file.Files.createDirectories(plotsOut);
    +289                java.nio.file.Files.createDirectories(reportsOut);
    +290                java.time.format.DateTimeFormatter df = java.time.format.DateTimeFormatter.ISO_DATE;
    +291                String dateStr = this.dateParam != null ? this.dateParam.format(df) : java.time.LocalDate.now().toString();
    +292                String baseName = "BrailleNote-" + sessionId + "-" + dateStr;
    +293
    +294                com.studentgui.apphelpers.Database.ResultsWithDates rwd = com.studentgui.apphelpers.Database.fetchLatestAssessmentResultsWithDates(this.studentNameParam, "BrailleNote", Integer.MAX_VALUE);
    +295                java.util.Map<String, java.nio.file.Path> groups = null;
    +296                String[] labels = new String[this.parts.length];
    +297                for (int i = 0; i < this.parts.length; i++) {
    +298                    labels[i] = this.parts[i][1];
    +299                }
    +300                if (rwd != null && rwd.rows != null && !rwd.rows.isEmpty()) {
    +301                    lineGraph.updateWithGroupedDataByDate(rwd.dates, rwd.rows, codes, labels);
    +302                    groups = lineGraph.saveGroupedCharts(plotsOut, baseName, 1000, 240);
    +303                    java.time.LocalDate headerDate = rwd.dates.get(rwd.dates.size() - 1);
    +304                    dateStr = headerDate.format(df);
    +305                } else {
    +306                    java.util.List<java.util.List<Integer>> rowsList = new java.util.ArrayList<>();
    +307                    java.util.List<Integer> latest = new java.util.ArrayList<>();
    +308                    for (int v : scores) {
    +309                        latest.add(v);
    +310                    }
    +311                    rowsList.add(latest);
    +312                    lineGraph.updateWithGroupedData(rowsList, codes);
    +313                    groups = lineGraph.saveGroupedCharts(plotsOut, baseName, 1000, 240);
    +314                }
    +315
    +316                if (groups == null) {
    +317                    groups = new java.util.LinkedHashMap<>();
    +318                }
    +319                StringBuilder md = new StringBuilder();
    +320                md.append("# ").append(this.studentNameParam == null ? "Unknown Student" : this.studentNameParam).append(" - ").append(dateStr).append("\n\n");
    +321                for (java.util.Map.Entry<String, java.nio.file.Path> e : groups.entrySet()) {
    +322                    md.append("## ").append(e.getKey()).append("\n\n");
    +323                    md.append("![](./").append(e.getValue().getFileName().toString()).append(")\n\n");
    +324                }
    +325                java.nio.file.Path mdFile = reportsOut.resolve(baseName + ".md");
    +326                String mdText = md.toString().replace("![](./", "![](../plots/");
    +327                java.nio.file.Files.writeString(mdFile, mdText, java.nio.charset.StandardCharsets.UTF_8);
    +328
    +329                try {
    +330                    String[] palette = JLineGraph.PALETTE_HEX;
    +331                    java.util.LinkedHashMap<String, java.util.List<Integer>> groupsIdx = new java.util.LinkedHashMap<>();
    +332                    for (int i = 0; i < codes.length; i++) {
    +333                        String code = codes[i];
    +334                        String grp = code != null && code.contains("_") ? code.split("_")[0] : code;
    +335                        groupsIdx.computeIfAbsent(grp, k -> new java.util.ArrayList<>()).add(i);
    +336                    }
    +337                    StringBuilder html = new StringBuilder();
    +338                    html.append("<!doctype html><html><head><meta charset=\"utf-8\"><title>");
    +339                    html.append(this.studentNameParam == null ? "Student Report" : this.studentNameParam).append(" - ").append(dateStr).append("</title>");
    +340                    html.append("<style>body{font-family:sans-serif;margin:20px;} img{max-width:100%;height:auto;border:1px solid #ccc;margin-bottom:8px;} .legend{max-height:160px;overflow:auto;border:1px solid #ddd;padding:8px;margin-bottom:24px;} .legend-item{display:flex;align-items:center;gap:8px;padding:4px 0;} .swatch{width:18px;height:12px;border:1px solid #333;display:inline-block}</style>");
    +341                    html.append("</head><body>");
    +342                    html.append("<h1>").append(this.studentNameParam == null ? "Unknown Student" : this.studentNameParam).append(" - ").append(dateStr).append("</h1>");
    +343                    for (java.util.Map.Entry<String, java.nio.file.Path> e2 : groups.entrySet()) {
    +344                        String grp = e2.getKey();
    +345                        String imgName = e2.getValue().getFileName().toString();
    +346                        html.append("<h2>").append(grp).append("</h2>");
    +347                        html.append("<div class=\"plot\"><img src=\"./").append(imgName).append("\" alt=\"").append(grp).append("\"></div>");
    +348                        java.util.List<Integer> idxs = groupsIdx.getOrDefault(grp, new java.util.ArrayList<>());
    +349                        html.append("<div class=\"legend\">");
    +350                        for (int s = 0; s < idxs.size(); s++) {
    +351                            int idx = idxs.get(s);
    +352                            String code = codes[idx];
    +353                            String human = this.parts[idx][1];
    +354                            String seriesName = code + " - " + human;
    +355                            String color = palette[s % palette.length];
    +356                            html.append("<div class=\"legend-item\">");
    +357                            html.append("<span class=\"swatch\" style=\"background:");
    +358                            html.append(color);
    +359                            html.append(";\"></span>");
    +360                            html.append("<div>");
    +361                            html.append(seriesName);
    +362                            html.append("</div></div>");
    +363                        }
    +364                        html.append("</div>");
    +365                    }
    +366                    html.append("</body></html>");
    +367                    java.nio.file.Path htmlFile = reportsOut.resolve(baseName + ".html");
    +368                    String htmlStr = html.toString().replace("src=\"./", "src=\"../plots/");
    +369                    java.nio.file.Files.writeString(htmlFile, htmlStr, java.nio.charset.StandardCharsets.UTF_8);
    +370                    LOG.info("Wrote BrailleNote HTML session report {}", htmlFile);
    +371                } catch (java.io.IOException ioex) {
    +372                    LOG.warn("Unable to write BrailleNote HTML report: {}", ioex.toString());
    +373                }
    +374            } catch (java.io.IOException ioe) {
    +375                LOG.warn("Unable to save BrailleNote per-phase plots or markdown report: {}", ioe.toString());
    +376            }
    +377        } catch (SQLException e) {
    +378            LOG.error("SQL error saving braille note data", e);
    +379            JOptionPane.showMessageDialog(this, "Database error saving BrailleNote data: " + e.getMessage(), "Database error", JOptionPane.ERROR_MESSAGE);
    +380        }
    +381    }
    +382
    +383    /**
    +384     * Query the most recent assessment sessions for this student and update
    +385     * the shared {@link JLineGraph} with the returned values.
    +386     */
    +387    private void refreshGraph() {
    +388        try {
    +389            List<List<Integer>> allSkillValues = com.studentgui.apphelpers.Database.fetchLatestAssessmentResults(studentNameParam, "BrailleNote", 5);
    +390            if (allSkillValues != null && !allSkillValues.isEmpty()) {
    +391                String[] codes = new String[this.parts.length];
    +392                for (int i = 0; i < this.parts.length; i++) {
    +393                    codes[i] = this.parts[i][0];
    +394                }
    +395                lineGraph.updateWithGroupedData(allSkillValues, codes);
    +396                    // Write to the consolidated per-run data dumps file when enabled
    +397                    if (Boolean.parseBoolean(com.studentgui.apphelpers.Settings.get("dump.enabled", "false"))) {
    +398                        try {
    +399                            String appHome = System.getProperty("APP_HOME", com.studentgui.apphelpers.Helpers.APP_HOME.toString());
    +400                            String ts = System.getProperty("LOG_TS", String.valueOf(java.time.Instant.now().getEpochSecond()));
    +401                            java.nio.file.Path logDir = java.nio.file.Paths.get(appHome).resolve("logs");
    +402                            java.nio.file.Files.createDirectories(logDir);
    +403                            java.nio.file.Path logFile = logDir.resolve("data_dumps_" + ts + ".log");
    +404                            StringBuilder sb = new StringBuilder();
    +405                            sb.append("[BrailleNote]").append(System.lineSeparator());
    +406                            sb.append(java.time.Instant.now().toString()).append(" - student=").append(this.studentNameParam).append(System.lineSeparator());
    +407                            sb.append("data=").append(allSkillValues.toString()).append(System.lineSeparator());
    +408                            sb.append(System.lineSeparator());
    +409                            java.nio.file.Files.writeString(logFile, sb.toString(), java.nio.charset.StandardCharsets.UTF_8, java.nio.file.StandardOpenOption.CREATE, java.nio.file.StandardOpenOption.APPEND);
    +410                        } catch (java.io.IOException ioe) {
    +411                            LOG.trace("Unable to write BrailleNote load log: {}", ioe.toString());
    +412                        }
    +413                    }
    +414            } else {
    +415                LOG.info("No data to plot.");
    +416                // Ensure the graph shows grouped placeholders matching the
    +417                // canonical assessment part ordering so the UI displays
    +418                // one subchart per P# prefix even with no sessions.
    +419                String[] codes = new String[this.parts.length];
    +420                for (int i = 0; i < this.parts.length; i++) {
    +421                    codes[i] = this.parts[i][0];
    +422                }
    +423                lineGraph.showEmptyGrouped(codes);
    +424            }
    +425        } catch (SQLException e) {
    +426            LOG.error("SQL error refreshing braille note graph", e);
    +427        }
    +428    }
    +429
    +430    @Override
    +431    public void dateChanged(LocalDate newDate) {
    +432        this.dateParam = newDate;
    +433        SwingUtilities.invokeLater(() -> {
    +434            refreshGraph();
    +435            updateTitleDate();
    +436        });
    +437    }
    +438
    +439    @Override
    +440    public void studentChanged(String newStudent) {
    +441        this.studentNameParam = newStudent;
    +442        SwingUtilities.invokeLater(() -> {
    +443            refreshGraph();
    +444            updateTitleDate();
    +445        });
    +446    }
    +447
    +448    private void updateTitleDate() {
    +449        try {
    +450            String dateStr = this.dateParam != null ? this.dateParam.toString() : java.time.LocalDate.now().toString();
    +451            this.titleLabel.setText(baseTitle + " - " + dateStr);
    +452        } catch (Exception ex) {
    +453            this.titleLabel.setText(baseTitle);
    +454        }
    +455    }
    +456    
    +457
    +458}
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    + + diff --git a/target/site/apidocs/src-html/com/studentgui/apppages/BrailleSense.html b/target/site/apidocs/src-html/com/studentgui/apppages/BrailleSense.html new file mode 100644 index 0000000..27f4d1f --- /dev/null +++ b/target/site/apidocs/src-html/com/studentgui/apppages/BrailleSense.html @@ -0,0 +1,424 @@ + + + + +Source code + + + + + + +
    +
    +
    001package com.studentgui.apppages;
    +002
    +003import java.awt.BorderLayout;
    +004import java.awt.Font;
    +005import java.awt.GridBagConstraints;
    +006import java.awt.GridBagLayout;
    +007import java.awt.Insets;
    +008import java.awt.event.ActionEvent;
    +009import java.awt.event.KeyEvent;
    +010import java.sql.SQLException;
    +011import java.time.LocalDate;
    +012import java.util.LinkedHashMap;
    +013import java.util.Map;
    +014
    +015import javax.swing.JButton;
    +016import javax.swing.JLabel;
    +017import javax.swing.JPanel;
    +018import javax.swing.JScrollPane;
    +019import javax.swing.SwingUtilities;
    +020
    +021import org.slf4j.Logger;
    +022import org.slf4j.LoggerFactory;
    +023
    +024import com.studentgui.uicomp.PhaseScoreField;
    +025
    +026/**
    +027 * HIMS BrailleSense productivity device proficiency assessment page.
    +028 *
    +029 * <p>Evaluates student competency with the HIMS BrailleSense family of refreshable braille
    +030 * notetakers (BrailleSense Polaris, BrailleSense 6, etc.) across 52 skills organized into
    +031 * 12 functional domains. The BrailleSense assessment structure mirrors {@link BrailleNote}
    +032 * to allow cross-device skill comparison.</p>
    +033 *
    +034 * <p><b>Device Family Context:</b> The BrailleSense is a portable braille notetaker with
    +035 * refreshable braille display, perkins-style keyboard, and integrated productivity software.
    +036 * It runs proprietary HIMS firmware and includes word processing, email, web browsing,
    +037 * media playback, and educational applications.</p>
    +038 *
    +039 * <p><b>Assessment Phases (12 domains, 52 skills):</b></p>
    +040 * <ul>
    +041 *   <li><b>Phase 1:</b> Device fundamentals (layout, setup, navigation, file management, core apps)</li>
    +042 *   <li><b>Phase 2:</b> Productivity suite (calendar, email, web, calculator, word processor)</li>
    +043 *   <li><b>Phase 3:</b> Advanced apps (presentations, code editor, third-party integration, braille I/O)</li>
    +044 *   <li><b>Phase 4:</b> Cloud integration and advanced file management</li>
    +045 *   <li><b>Phase 5:</b> Collaboration, export/import, printing, backup workflows</li>
    +046 *   <li><b>Phase 6:</b> App installation, updates, troubleshooting</li>
    +047 *   <li><b>Phase 7:</b> Automation (custom shortcuts, macros, scripting)</li>
    +048 *   <li><b>Phase 8:</b> Peripheral connectivity (Bluetooth, USB, displays, audio/video)</li>
    +049 *   <li><b>Phase 9:</b> Security, user accounts, parental controls, network settings</li>
    +050 *   <li><b>Phase 10:</b> Speech customization (TTS settings, voice profiles, languages)</li>
    +051 *   <li><b>Phase 11:</b> Device maintenance (firmware, diagnostics, logs, support, warranty)</li>
    +052 *   <li><b>Phase 12:</b> Community resources (online help, forums, feedback channels)</li>
    +053 * </ul>
    +054 *
    +055 * <p><b>Data Management and Report Generation:</b></p>
    +056 * <ul>
    +057 *   <li>Scores captured via {@link PhaseScoreField} components (integer 0–4 typical)</li>
    +058 *   <li>Persisted to normalized schema via {@link com.studentgui.apphelpers.Database#insertAssessmentResults}</li>
    +059 *   <li>JSON export: {@code StudentDataFiles/<student>/Sessions/BrailleSense/BrailleSense-<sessionId>-<timestamp>.json}</li>
    +060 *   <li>Phase-grouped time-series plots: {@code plots/BrailleSense-<sessionId>-<date>-P<N>.png} (12 phase groups)</li>
    +061 *   <li>Markdown and HTML reports with embedded plots and color-coded legends</li>
    +062 * </ul>
    +063 *
    +064 * <p>The shared {@link JLineGraph} visualizes recent session trends grouped by phase prefix.
    +065 * This page operates on static student/date parameters and does not implement listener interfaces.</p>
    +066 *
    +067 * @see com.studentgui.apphelpers.Database
    +068 * @see JLineGraph
    +069 * @see PhaseScoreField
    +070 * @see BrailleNote
    +071 */
    +072public class BrailleSense extends JPanel {
    +073    private static final Logger LOG = LoggerFactory.getLogger(BrailleSense.class);
    +074    /** Map of assessment part codes to their input components. */
    +075    private final Map<String, PhaseScoreField> inputs = new LinkedHashMap<>();
    +076    /** Canonical assessment parts for BrailleSense. */
    +077    private final String[][] parts;
    +078    /** Selected student display name (may be null). */
    +079    private final String studentNameParam;
    +080    /** Date associated with the current session. */
    +081    private final LocalDate dateParam;
    +082    /** Shared graph component used to visualize recent results. */
    +083    private final JLineGraph graph;
    +084
    +085    /**
    +086     * Create a BrailleSense page bound to the provided student and date.
    +087     *
    +088     * @param studentName selected student name (may be null until selection)
    +089     * @param date session date to associate with persisted progress rows
    +090     * @param graph shared graph component used to plot recent results
    +091     */
    +092    public BrailleSense(String studentName, LocalDate date, JLineGraph graph) {
    +093    this.studentNameParam = (studentName == null || studentName.trim().isEmpty()) ? com.studentgui.apphelpers.Helpers.defaultStudent() : studentName;
    +094        this.dateParam = date;
    +095        this.graph = graph;
    +096        setLayout(new BorderLayout());
    +097
    +098        // create a data entry panel that mirrors BrailleNote's layout so alignment is identical
    +099        JPanel dataEntryPanel = new JPanel(new GridBagLayout());
    +100        JPanel view = new JPanel(new BorderLayout());
    +101        view.add(dataEntryPanel, BorderLayout.NORTH);
    +102        view.setBorder(javax.swing.BorderFactory.createEmptyBorder(20, 20, 20, 20));
    +103        JScrollPane dataEntryScrollPane = new JScrollPane(view);
    +104        dataEntryScrollPane.setVerticalScrollBarPolicy(javax.swing.JScrollPane.VERTICAL_SCROLLBAR_AS_NEEDED);
    +105        dataEntryScrollPane.setHorizontalScrollBarPolicy(javax.swing.JScrollPane.HORIZONTAL_SCROLLBAR_NEVER);
    +106        dataEntryScrollPane.getAccessibleContext().setAccessibleName("BrailleSense data entry scroll pane");
    +107
    +108        GridBagConstraints gbc = new GridBagConstraints();
    +109        gbc.insets = new Insets(2, 2, 2, 2);
    +110        gbc.fill = GridBagConstraints.HORIZONTAL;
    +111        gbc.weightx = 1.0;
    +112        gbc.weighty = 0.0;
    +113
    +114        JLabel titleLabel = new JLabel("BrailleSense Skills");
    +115        // Use an explicit font so theme changes don't alter the title appearance
    +116        titleLabel.setFont(new Font(Font.SANS_SERIF, Font.BOLD, 16));
    +117        gbc.gridx = 0;
    +118        gbc.gridy = 0;
    +119        gbc.gridwidth = GridBagConstraints.REMAINDER;
    +120        dataEntryPanel.add(titleLabel, gbc);
    +121
    +122        gbc.gridy = 1;
    +123        gbc.gridwidth = GridBagConstraints.REMAINDER;
    +124        gbc.ipady = 20;
    +125        dataEntryPanel.add(new JPanel(), gbc);
    +126
    +127        this.parts = new String[][]{
    +128                {"P1_1", "1.1 Physical Layout"}, {"P1_2", "1.2 Setup/Universal Commands"}, {"P1_3", "1.3 BNT+ Navigation"}, {"P1_4", "1.4 File Management"}, {"P1_5", "1.5 Word Processor"}, {"P1_6", "1.6 Email"}, {"P1_7", "1.7 Internet"}, {"P1_8", "1.8 Calculator"}, {"P1_9", "1.9 KeyMath"},
    +129                {"P2_1", "2.1 Calendar"}, {"P2_2", "2.2 KeyBRF"}, {"P2_3", "2.3 KeyFiles"}, {"P2_4", "2.4 KeyMail"}, {"P2_5", "2.5 KeyWeb"}, {"P2_6", "2.6 KeyCalc"}, {"P2_7", "2.7 KeyWord"},
    +130                {"P3_1", "3.1 KeySlides"}, {"P3_2", "3.2 KeyCode"}, {"P3_3", "3.3 Third Party Apps"}, {"P3_4", "3.4 Braille Input"}, {"P3_5", "3.5 Braille Output"}, {"P3_6", "3.6 Settings"}, {"P3_7", "3.7 Accessibility"},
    +131                {"P4_1", "4.1 Advanced File Management"}, {"P4_2", "4.2 Cloud Integration"}, {"P4_3", "4.3 Device Maintenance"},
    +132                {"P5_1", "5.1 Collaboration"}, {"P5_2", "5.2 Export/Import"}, {"P5_3", "5.3 Printing"}, {"P5_4", "5.4 Backup"},
    +133                {"P6_1", "6.1 App Installation"}, {"P6_2", "6.2 App Updates"}, {"P6_3", "6.3 Troubleshooting"},
    +134                {"P7_1", "7.1 Custom Shortcuts"}, {"P7_2", "7.2 Macros"}, {"P7_3", "7.3 Scripting"}, {"P7_4", "7.4 Automation"},
    +135                {"P8_1", "8.1 Bluetooth Devices"}, {"P8_2", "8.2 USB Devices"}, {"P8_3", "8.3 External Displays"}, {"P8_4", "8.4 Audio Output"}, {"P8_5", "8.5 Video Output"},
    +136                {"P9_1", "9.1 Security"}, {"P9_2", "9.2 User Accounts"}, {"P9_3", "9.3 Parental Controls"}, {"P9_4", "9.4 Network Settings"},
    +137                {"P10_1", "10.1 Speech Settings"}, {"P10_2", "10.2 Voice Profiles"}, {"P10_3", "10.3 Language Support"},
    +138                {"P11_1", "11.1 Firmware Updates"}, {"P11_2", "11.2 Diagnostics"}, {"P11_3", "11.3 Logs"}, {"P11_4", "11.4 Support"}, {"P11_5", "11.5 Warranty"},
    +139                {"P12_1", "12.1 Community Resources"}, {"P12_2", "12.2 Online Help"}, {"P12_3", "12.3 User Forums"}, {"P12_4", "12.4 Feedback"}
    +140        };
    +141
    +142        // compute pixel width using font metrics so labels align precisely
    +143        String[] labels = java.util.Arrays.stream(this.parts).map(x -> x[1]).toArray(String[]::new);
    +144        int maxPx = com.studentgui.uicomp.PhaseScoreField.computeMaxLabelPixelWidth(titleLabel.getFont(), labels);
    +145        com.studentgui.uicomp.PhaseScoreField.setGlobalLabelWidth(Math.min(360, Math.max(200, maxPx + 50)));
    +146        int row = 1;
    +147        for (String[] def : this.parts) {
    +148            gbc.gridx = 0;
    +149            gbc.gridy = row;
    +150            gbc.gridwidth = 2;
    +151            PhaseScoreField tf = new PhaseScoreField(def[1], 0);
    +152            tf.setName("braillesense_" + def[0]);
    +153            tf.getAccessibleContext().setAccessibleName(def[1]);
    +154            tf.setToolTipText("Enter score for " + def[1]);
    +155            dataEntryPanel.add(tf, gbc);
    +156            inputs.put(def[0], tf);
    +157            row++;
    +158        }
    +159
    +160        // Place Submit and Open Latest side-by-side to match IOS/ScreenReader styling
    +161        gbc.gridx = 0;
    +162        gbc.gridy = row;
    +163        gbc.gridwidth = 1;
    +164        gbc.anchor = GridBagConstraints.WEST;
    +165        JButton submit = new JButton("Submit Data");
    +166        submit.setPreferredSize(new java.awt.Dimension(0, 32));
    +167        submit.addActionListener((ActionEvent e) -> save());
    +168        submit.setMnemonic(KeyEvent.VK_S);
    +169        submit.setToolTipText("Save BrailleSense scores (Alt+S)");
    +170        submit.getAccessibleContext().setAccessibleName("Submit BrailleSense Data");
    +171        submit.setName("braillesense_submit");
    +172        dataEntryPanel.add(submit, gbc);
    +173
    +174        gbc.gridx = 1;
    +175        gbc.gridwidth = 1;
    +176        gbc.anchor = GridBagConstraints.WEST;
    +177        JButton openLatest = new JButton("Open Latest Plot");
    +178        openLatest.setPreferredSize(new java.awt.Dimension(0, 32));
    +179        openLatest.addActionListener((ActionEvent e) -> {
    +180            java.nio.file.Path p = com.studentgui.apphelpers.Helpers.latestPlotPath(this.studentNameParam, "BrailleSense");
    +181            if (p == null) {
    +182                com.studentgui.apphelpers.UiNotifier.show("No BrailleSense plot found for student");
    +183            } else {
    +184                try {
    +185                    java.awt.Desktop.getDesktop().open(p.toFile());
    +186                } catch (java.io.IOException | UnsupportedOperationException | SecurityException ex) {
    +187                    com.studentgui.apphelpers.UiNotifier.show("Unable to open plot: " + p.getFileName().toString());
    +188                }
    +189            }
    +190        });
    +191        dataEntryPanel.add(openLatest, gbc);
    +192
    +193        // Filler to consume remaining horizontal space
    +194        gbc.gridx = 2;
    +195        gbc.gridwidth = GridBagConstraints.REMAINDER;
    +196        gbc.anchor = GridBagConstraints.WEST;
    +197        dataEntryPanel.add(new JPanel(), gbc);
    +198
    +199        dataEntryScrollPane.getAccessibleContext().setAccessibleName("BrailleSense data entry scroll pane");
    +200        add(dataEntryScrollPane, BorderLayout.CENTER);
    +201        add(graph, BorderLayout.SOUTH);
    +202        SwingUtilities.invokeLater(() -> {
    +203            dataEntryPanel.setPreferredSize(dataEntryPanel.getPreferredSize());
    +204            revalidate();
    +205        });
    +206        SwingUtilities.invokeLater(() -> {
    +207            for (var e : inputs.values()) {
    +208                LOG.debug("BrailleSense field {} labelWidth={} spinnerX={} gap={}", e.getLabel(), e.getLabelWrapWidth(), e.getSpinnerX(), e.getActualGap());
    +209            }
    +210        });
    +211        com.studentgui.apphelpers.Helpers.createFolderHierarchy();
    +212        initParts();
    +213    }
    +214
    +215    /**
    +216     * Ensure the database contains the progress-type and assessment part rows
    +217     * for BrailleSense. Safe to call repeatedly.
    +218     */
    +219    private void initParts() {
    +220        try {
    +221            int ptId = com.studentgui.apphelpers.Database.getOrCreateProgressType("BrailleSense");
    +222            String[] codes = inputs.keySet().toArray(String[]::new);
    +223            com.studentgui.apphelpers.Database.ensureAssessmentParts(ptId, codes);
    +224        } catch (SQLException ex) {
    +225            LOG.error("Error ensuring braillesense parts", ex);
    +226        }
    +227    }
    +228
    +229    /**
    +230     * Persist the current inputs as a new progress session for the selected
    +231     * student. Non-integer input is treated as zero.
    +232     */
    +233    private void save() {
    +234        try {
    +235            int studentId = com.studentgui.apphelpers.Database.getOrCreateStudent(studentNameParam);
    +236            int ptId = com.studentgui.apphelpers.Database.getOrCreateProgressType("BrailleSense");
    +237            int sessionId = com.studentgui.apphelpers.Database.createProgressSession(studentId, ptId, dateParam);
    +238            String[] codes = inputs.keySet().toArray(String[]::new);
    +239            int[] scores = new int[codes.length];
    +240            for (int i = 0; i < codes.length; i++) {
    +241                scores[i] = inputs.get(codes[i]).getValue();
    +242            }
    +243            com.studentgui.apphelpers.Database.insertAssessmentResults(sessionId, ptId, codes, scores);
    +244            LOG.info("BrailleSense data saved for {}", studentNameParam);
    +245            com.studentgui.apphelpers.UiNotifier.show("BrailleSense data saved.");
    +246            com.studentgui.apphelpers.dto.AssessmentPayload payload = new com.studentgui.apphelpers.dto.AssessmentPayload(sessionId, codes, scores);
    +247            java.nio.file.Path jsonOut = com.studentgui.apphelpers.SessionJsonWriter.writeSessionJson(this.studentNameParam, "BrailleSense", payload, sessionId);
    +248            if (jsonOut == null) {
    +249                LOG.warn("Unable to save BrailleSense session JSON for sessionId={}", sessionId);
    +250            }
    +251            try {
    +252                java.nio.file.Path plotsOut = com.studentgui.apphelpers.Helpers.studentPlotsDir(this.studentNameParam);
    +253                java.nio.file.Path reportsOut = com.studentgui.apphelpers.Helpers.studentReportsDir(this.studentNameParam);
    +254                java.nio.file.Files.createDirectories(plotsOut);
    +255                java.nio.file.Files.createDirectories(reportsOut);
    +256                java.time.format.DateTimeFormatter df = java.time.format.DateTimeFormatter.ISO_DATE;
    +257                String dateStr = this.dateParam != null ? this.dateParam.format(df) : java.time.LocalDate.now().toString();
    +258                String baseName = "BrailleSense-" + sessionId + "-" + dateStr;
    +259
    +260                com.studentgui.apphelpers.Database.ResultsWithDates rwd = com.studentgui.apphelpers.Database.fetchLatestAssessmentResultsWithDates(this.studentNameParam, "BrailleSense", Integer.MAX_VALUE);
    +261                java.util.Map<String, java.nio.file.Path> groups = null;
    +262                String[] labels = java.util.Arrays.stream(this.parts).map(x -> x[1]).toArray(String[]::new);
    +263                if (rwd != null && rwd.rows != null && !rwd.rows.isEmpty()) {
    +264                    graph.updateWithGroupedDataByDate(rwd.dates, rwd.rows, codes, labels);
    +265                    groups = graph.saveGroupedCharts(plotsOut, baseName, 1000, 240);
    +266                    java.time.LocalDate headerDate = rwd.dates.get(rwd.dates.size() - 1);
    +267                    dateStr = headerDate.format(df);
    +268                } else {
    +269                    java.util.List<java.util.List<Integer>> rowsList = new java.util.ArrayList<>();
    +270                    java.util.List<Integer> latest = new java.util.ArrayList<>();
    +271                    for (int i = 0; i < codes.length; i++) {
    +272                        latest.add(inputs.get(codes[i]).getValue());
    +273                    }
    +274                    rowsList.add(latest);
    +275                    graph.updateWithGroupedData(rowsList, codes);
    +276                    groups = graph.saveGroupedCharts(plotsOut, baseName, 1000, 240);
    +277                }
    +278
    +279                if (groups == null) {
    +280                    groups = new java.util.LinkedHashMap<>();
    +281                }
    +282                StringBuilder md = new StringBuilder();
    +283                md.append("# ").append(this.studentNameParam == null ? "Unknown Student" : this.studentNameParam).append(" - ").append(dateStr).append("\n\n");
    +284                for (java.util.Map.Entry<String, java.nio.file.Path> e : groups.entrySet()) {
    +285                    md.append("## ").append(e.getKey()).append("\n\n");
    +286                    md.append("![](./").append(e.getValue().getFileName().toString()).append(")\n\n");
    +287                }
    +288                // adjust markdown image links to point to the plots folder relative to reports
    +289                java.lang.String mdText = md.toString().replace("![](./", "![](../plots/");
    +290                java.nio.file.Path mdFile = reportsOut.resolve(baseName + ".md");
    +291                java.nio.file.Files.writeString(mdFile, mdText, java.nio.charset.StandardCharsets.UTF_8);
    +292
    +293                try {
    +294                    String[] palette = JLineGraph.PALETTE_HEX;
    +295                    java.util.LinkedHashMap<String, java.util.List<Integer>> groupsIdx = new java.util.LinkedHashMap<>();
    +296                    for (int i = 0; i < codes.length; i++) {
    +297                        String code = codes[i];
    +298                        String grp = code != null && code.contains("_") ? code.split("_")[0] : code;
    +299                        groupsIdx.computeIfAbsent(grp, k -> new java.util.ArrayList<>()).add(i);
    +300                    }
    +301                    StringBuilder html = new StringBuilder();
    +302                    html.append("<!doctype html><html><head><meta charset=\"utf-8\"><title>");
    +303                    html.append(this.studentNameParam == null ? "Student Report" : this.studentNameParam).append(" - ").append(dateStr).append("</title>");
    +304                    html.append("<style>body{font-family:sans-serif;margin:20px;} img{max-width:100%;height:auto;border:1px solid #ccc;margin-bottom:8px;} .legend{max-height:160px;overflow:auto;border:1px solid #ddd;padding:8px;margin-bottom:24px;} .legend-item{display:flex;align-items:center;gap:8px;padding:4px 0;} .swatch{width:18px;height:12px;border:1px solid #333;display:inline-block}</style>");
    +305                    html.append("</head><body>");
    +306                    html.append("<h1>").append(this.studentNameParam == null ? "Unknown Student" : this.studentNameParam).append(" - ").append(dateStr).append("</h1>");
    +307                    for (java.util.Map.Entry<String, java.nio.file.Path> e2 : groups.entrySet()) {
    +308                        String grp = e2.getKey();
    +309                        String imgName = e2.getValue().getFileName().toString();
    +310                        html.append("<h2>").append(grp).append("</h2>");
    +311                        html.append("<div class=\"plot\"><img src=\"../plots/").append(imgName).append("\" alt=\"").append(grp).append("\"></div>");
    +312                        java.util.List<Integer> idxs = groupsIdx.getOrDefault(grp, new java.util.ArrayList<>());
    +313                        html.append("<div class=\"legend\">");
    +314                        for (int s = 0; s < idxs.size(); s++) {
    +315                            int idx = idxs.get(s);
    +316                            String code = codes[idx];
    +317                            String human = this.parts[idx][1];
    +318                            String seriesName = code + " - " + human;
    +319                            String color = palette[s % palette.length];
    +320                            html.append("<div class=\"legend-item\">");
    +321                            html.append("<span class=\"swatch\" style=\"background:");
    +322                            html.append(color);
    +323                            html.append(";\"></span>");
    +324                            html.append("<div>");
    +325                            html.append(seriesName);
    +326                            html.append("</div></div>");
    +327                        }
    +328                        html.append("</div>");
    +329                    }
    +330                    html.append("</body></html>");
    +331                    java.nio.file.Path htmlFile = reportsOut.resolve(baseName + ".html");
    +332                    java.nio.file.Files.writeString(htmlFile, html.toString(), java.nio.charset.StandardCharsets.UTF_8);
    +333                    LOG.info("Wrote BrailleSense HTML session report {}", htmlFile);
    +334                } catch (java.io.IOException ioex) {
    +335                    LOG.warn("Unable to write BrailleSense HTML report: {}", ioex.toString());
    +336                }
    +337            } catch (java.io.IOException ioe) {
    +338                LOG.warn("Unable to save BrailleSense per-phase plots or markdown report: {}", ioe.toString());
    +339            }
    +340        } catch (SQLException ex) {
    +341            LOG.error("Error saving braillesense data", ex);
    +342        }
    +343    }
    +344
    +345    // plotting is handled via submit/save which updates the shared graph and saves a static PNG
    +346}
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    + + diff --git a/target/site/apidocs/src-html/com/studentgui/apppages/CVI.html b/target/site/apidocs/src-html/com/studentgui/apppages/CVI.html new file mode 100644 index 0000000..202dca3 --- /dev/null +++ b/target/site/apidocs/src-html/com/studentgui/apppages/CVI.html @@ -0,0 +1,380 @@ + + + + +Source code + + + + + + +
    +
    +
    001package com.studentgui.apppages;
    +002
    +003import java.awt.BorderLayout;
    +004import java.awt.Font;
    +005import java.awt.GridBagConstraints;
    +006import java.awt.GridBagLayout;
    +007import java.awt.Insets;
    +008import java.awt.event.ActionEvent;
    +009import java.awt.event.KeyEvent;
    +010import java.sql.SQLException;
    +011import java.time.LocalDate;
    +012import java.util.LinkedHashMap;
    +013import java.util.Map;
    +014
    +015import javax.swing.JButton;
    +016import javax.swing.JLabel;
    +017import javax.swing.JPanel;
    +018import javax.swing.JScrollPane;
    +019import javax.swing.SwingUtilities;
    +020
    +021import org.slf4j.Logger;
    +022import org.slf4j.LoggerFactory;
    +023
    +024import com.studentgui.uicomp.PhaseScoreField;
    +025
    +026/**
    +027 * Cortical Visual Impairment (CVI) assessment page.
    +028 *
    +029 * <p>Provides a structured scoring interface for evaluating the 10 characteristic behaviors
    +030 * associated with Cortical Visual Impairment as defined in the Roman-Lanzi CVI Range assessment
    +031 * framework. Skills are organized into two functional clusters:</p>
    +032 *
    +033 * <ul>
    +034 *   <li><b>Phase 1 (P1_1–P1_6): Primary CVI Characteristics</b>
    +035 *     <ul>
    +036 *       <li><b>Color Preference:</b> Preference for high-saturation colors (red, yellow)</li>
    +037 *       <li><b>Need for Movement:</b> Improved visual attention with motion</li>
    +038 *       <li><b>Latency:</b> Delayed visual response times</li>
    +039 *       <li><b>Field Preference:</b> Asymmetric visual field usage patterns</li>
    +040 *       <li><b>Visual Complexity:</b> Difficulty with cluttered/busy visual environments</li>
    +041 *       <li><b>Nonpurposeful Gaze:</b> Reduced sustained visual fixation</li>
    +042 *     </ul>
    +043 *   </li>
    +044 *   <li><b>Phase 2 (P2_1–P2_4): Secondary/Environmental Characteristics</b>
    +045 *     <ul>
    +046 *       <li><b>Distance Viewing:</b> Reduced effectiveness at distance</li>
    +047 *       <li><b>Atypical Reflexes:</b> Blink-to-threat, light reflex variations</li>
    +048 *       <li><b>Visual Novelty:</b> Preference for familiar objects/environments</li>
    +049 *       <li><b>Visual Reach:</b> Difficulty localizing and reaching toward objects</li>
    +050 *     </ul>
    +051 *   </li>
    +052 * </ul>
    +053 *
    +054 * <p><b>Scoring and Interpretation:</b> Each characteristic is typically scored on a 0–10 scale
    +055 * representing frequency/severity of the behavior. Higher scores may indicate greater impact
    +056 * depending on the specific assessment protocol in use. Consult the Roman-Lanzi CVI Range manual
    +057 * for standardized scoring guidelines.</p>
    +058 *
    +059 * <p><b>Data Management:</b></p>
    +060 * <ul>
    +061 *   <li>Scores captured via {@link PhaseScoreField} components with integer validation</li>
    +062 *   <li>Submit button persists to database via {@link com.studentgui.apphelpers.Database#insertAssessmentResults}</li>
    +063 *   <li>Session JSON exported to {@code StudentDataFiles/<student>/Sessions/CVI/CVI-<sessionId>-<timestamp>.json}</li>
    +064 *   <li>Time-series plots generated per phase group and saved to {@code plots/} directory</li>
    +065 *   <li>Markdown and HTML reports generated with embedded plots and color-coded legends</li>
    +066 * </ul>
    +067 *
    +068 * <p>The shared {@link JLineGraph} component visualizes trends across multiple sessions,
    +069 * grouped by phase to separate primary and secondary characteristics. This page does not
    +070 * implement listener interfaces as it operates on static student/date parameters.</p>
    +071 *
    +072 * @see com.studentgui.apphelpers.Database
    +073 * @see JLineGraph
    +074 * @see PhaseScoreField
    +075 */
    +076public class CVI extends JPanel {
    +077    private static final Logger LOG = LoggerFactory.getLogger(CVI.class);
    +078    /** Mapping of assessment part codes to their input components. */
    +079    private final Map<String, PhaseScoreField> inputs = new LinkedHashMap<>();
    +080
    +081    /** Selected student display name (may be null) used when saving or plotting. */
    +082    private final String studentNameParam;
    +083
    +084    /** Session date to associate with saved CVI progress entries. */
    +085    private final LocalDate dateParam;
    +086
    +087    /** Shared graph component used to visualize recent CVI results. */
    +088    private final JLineGraph graph;
    +089
    +090    /**
    +091     * Construct the CVI page bound to the selected student and session date.
    +092     *
    +093     * @param studentName selected student name (may be null)
    +094     * @param date session date to use when creating progress sessions
    +095     * @param graph shared graph used to visualize recent results
    +096     */
    +097    public CVI(String studentName, LocalDate date, JLineGraph graph) {
    +098    this.studentNameParam = (studentName == null || studentName.trim().isEmpty()) ? com.studentgui.apphelpers.Helpers.defaultStudent() : studentName;
    +099        this.dateParam = date;
    +100        this.graph = graph;
    +101    setLayout(new BorderLayout());
    +102    JPanel panel = new JPanel(new GridBagLayout());
    +103    JPanel view = new JPanel(new BorderLayout());
    +104    view.add(panel, BorderLayout.NORTH);
    +105    view.setBorder(javax.swing.BorderFactory.createEmptyBorder(20,20,20,20));
    +106    JScrollPane scroll = new JScrollPane(view);
    +107    GridBagConstraints gbc = new GridBagConstraints(); gbc.insets=new Insets(2,2,2,2); gbc.fill = GridBagConstraints.HORIZONTAL; gbc.anchor = GridBagConstraints.NORTHWEST;
    +108
    +109    JLabel title = new JLabel("CVI Progression");
    +110    title.setFont(title.getFont().deriveFont(Font.BOLD,16));
    +111    title.getAccessibleContext().setAccessibleName("CVI Progression Title");
    +112    title.setHorizontalAlignment(JLabel.LEFT);
    +113    gbc.gridx=0; gbc.gridy=0; gbc.gridwidth=2; panel.add(title, gbc);
    +114
    +115    String[][] parts = new String[][]{{"P1_1","Color Preference"},{"P1_2","Need for Movement"},{"P1_3","Latency"},{"P1_4","Field Preference"},{"P1_5","Visual Complexity"},{"P1_6","Nonpurposeful Gaze"},{"P2_1","Distance Viewing"},{"P2_2","Atypical Reflexes"},{"P2_3","Visual Novelty"},{"P2_4","Visual Reach"}};
    +116    String[] labels = java.util.Arrays.stream(parts).map(x->x[1]).toArray(String[]::new);
    +117            int maxPx = com.studentgui.uicomp.PhaseScoreField.computeMaxLabelPixelWidth(title.getFont(), labels);
    +118            com.studentgui.uicomp.PhaseScoreField.setGlobalLabelWidth(Math.min(320, Math.max(140, maxPx + 50)));
    +119    int row = 1;
    +120    for (String[] pdef: parts) {
    +121        gbc.gridx = 0; gbc.gridy = row; gbc.gridwidth = 2;
    +122        PhaseScoreField tf = new PhaseScoreField(pdef[1], 0);
    +123        tf.setToolTipText("Enter whole number score for " + pdef[1]);
    +124        tf.getAccessibleContext().setAccessibleName(pdef[1]);
    +125        tf.setName("cvi_" + pdef[0]);
    +126        panel.add(tf, gbc);
    +127        inputs.put(pdef[0], tf);
    +128        row++;
    +129    }
    +130
    +131    // Two side-by-side buttons: Submit Data (save + save PNG) and Open Latest Plot
    +132    gbc.gridx = 0; gbc.gridy = row; gbc.gridwidth = 1; gbc.anchor = GridBagConstraints.WEST;
    +133    JButton submit = new JButton("Submit Data");
    +134    submit.setPreferredSize(new java.awt.Dimension(0, 32));
    +135    submit.addActionListener((ActionEvent e) -> save());
    +136    submit.setToolTipText("Save CVI assessment for selected student (Alt+S)");
    +137    submit.setMnemonic(KeyEvent.VK_S);
    +138    submit.getAccessibleContext().setAccessibleName("Submit CVI Data");
    +139    submit.setName("cvi_submit");
    +140    panel.add(submit, gbc);
    +141
    +142    gbc.gridx = 1; gbc.gridwidth = 1; gbc.anchor = GridBagConstraints.WEST;
    +143    JButton openLatest = new JButton("Open Latest Plot");
    +144    openLatest.setPreferredSize(new java.awt.Dimension(0, 32));
    +145    openLatest.addActionListener((ActionEvent e) -> {
    +146        java.nio.file.Path plotPath = com.studentgui.apphelpers.Helpers.latestPlotPath(this.studentNameParam, "CVI");
    +147        if (plotPath == null) com.studentgui.apphelpers.UiNotifier.show("No CVI plot found for student");
    +148        else {
    +149                // Do not auto-open CVI plot on startup; only save it. Opening is handled
    +150                // by explicit user actions (Open Latest Plot).
    +151        }
    +152    });
    +153    panel.add(openLatest, gbc);
    +154    gbc.gridwidth = 1;
    +155
    +156        add(scroll, BorderLayout.CENTER); add(graph, BorderLayout.SOUTH);
    +157        SwingUtilities.invokeLater(()->{ panel.setPreferredSize(panel.getPreferredSize()); revalidate(); });
    +158        com.studentgui.apphelpers.Helpers.createFolderHierarchy();
    +159        initParts();
    +160    }
    +161
    +162    /**
    +163     * Ensure the CVI progress-type and part rows exist in the database.
    +164     */
    +165    private void initParts() {
    +166        try {
    +167            int ptId = com.studentgui.apphelpers.Database.getOrCreateProgressType("CVI");
    +168            java.util.Set<String> keys = inputs.keySet();
    +169            String[] codes = new String[keys.size()];
    +170            int kidx = 0;
    +171            for (String k : keys) {
    +172                codes[kidx++] = k;
    +173            }
    +174            com.studentgui.apphelpers.Database.ensureAssessmentParts(ptId, codes);
    +175        } catch (SQLException ex) {
    +176            LOG.error("Error ensuring CVI parts", ex);
    +177        }
    +178    }
    +179
    +180    /**
    +181     * Validate inputs and persist them as a new CVI progress session for the
    +182     * selected student.
    +183     */
    +184    private void save() {
    +185        if (studentNameParam == null || studentNameParam.trim().isEmpty()) {
    +186            com.studentgui.apphelpers.UiNotifier.show("Please select a student before saving CVI data.");
    +187            return;
    +188        }
    +189
    +190        try {
    +191            int studentId = com.studentgui.apphelpers.Database.getOrCreateStudent(studentNameParam);
    +192            int ptId = com.studentgui.apphelpers.Database.getOrCreateProgressType("CVI");
    +193            int sessionId = com.studentgui.apphelpers.Database.createProgressSession(studentId, ptId, dateParam);
    +194            java.util.Set<String> keys = inputs.keySet();
    +195            String[] codes = new String[keys.size()];
    +196            int kidx = 0;
    +197            for (String k : keys) codes[kidx++] = k;
    +198            int[] scores = new int[codes.length];
    +199            for (int i = 0; i < codes.length; i++) {
    +200                scores[i] = inputs.get(codes[i]).getValue();
    +201            }
    +202            com.studentgui.apphelpers.Database.insertAssessmentResults(sessionId, ptId, codes, scores);
    +203            LOG.info("CVI data saved for {}", studentNameParam);
    +204            com.studentgui.apphelpers.UiNotifier.show("CVI data saved.");
    +205            com.studentgui.apphelpers.dto.AssessmentPayload payload = new com.studentgui.apphelpers.dto.AssessmentPayload(sessionId, codes, scores);
    +206            java.nio.file.Path jsonOut = com.studentgui.apphelpers.SessionJsonWriter.writeSessionJson(this.studentNameParam, "CVI", payload, sessionId);
    +207            if (jsonOut == null) LOG.warn("Unable to save CVI session JSON for sessionId={}", sessionId);
    +208            try {
    +209                java.nio.file.Path plotsOut = com.studentgui.apphelpers.Helpers.studentPlotsDir(this.studentNameParam);
    +210                java.nio.file.Path reportsOut = com.studentgui.apphelpers.Helpers.studentReportsDir(this.studentNameParam);
    +211                java.nio.file.Files.createDirectories(plotsOut);
    +212                java.nio.file.Files.createDirectories(reportsOut);
    +213                java.time.format.DateTimeFormatter df = java.time.format.DateTimeFormatter.ISO_DATE;
    +214                String dateStr = this.dateParam != null ? this.dateParam.format(df) : java.time.LocalDate.now().toString();
    +215                String baseName = "CVI-" + sessionId + "-" + dateStr;
    +216
    +217                com.studentgui.apphelpers.Database.ResultsWithDates rwd = com.studentgui.apphelpers.Database.fetchLatestAssessmentResultsWithDates(this.studentNameParam, "CVI", Integer.MAX_VALUE);
    +218                java.util.Map<String, java.nio.file.Path> groups = null;
    +219                String[] labels = new String[codes.length];
    +220                for (int i = 0; i < codes.length; i++) labels[i] = inputs.get(codes[i]).getLabel();
    +221                if (rwd != null && rwd.rows != null && !rwd.rows.isEmpty()) {
    +222                    graph.updateWithGroupedDataByDate(rwd.dates, rwd.rows, codes, labels);
    +223                    groups = graph.saveGroupedCharts(plotsOut, baseName, 1000, 240);
    +224                    java.time.LocalDate headerDate = rwd.dates.get(rwd.dates.size() - 1);
    +225                    dateStr = headerDate.format(df);
    +226                } else {
    +227                    java.util.List<java.util.List<Integer>> rowsList = new java.util.ArrayList<>();
    +228                    java.util.List<Integer> latest = new java.util.ArrayList<>();
    +229                    for (String c : codes) {
    +230                        latest.add(inputs.get(c).getValue());
    +231                    }
    +232                    rowsList.add(latest);
    +233                    graph.updateWithGroupedData(rowsList, codes);
    +234                    groups = graph.saveGroupedCharts(plotsOut, baseName, 1000, 240);
    +235                }
    +236
    +237                if (groups == null) groups = new java.util.LinkedHashMap<>();
    +238                StringBuilder md = new StringBuilder();
    +239                md.append("# ").append(this.studentNameParam == null ? "Unknown Student" : this.studentNameParam).append(" - ").append(dateStr).append("\n\n");
    +240                for (java.util.Map.Entry<String, java.nio.file.Path> e : groups.entrySet()) {
    +241                    md.append("## ").append(e.getKey()).append("\n\n");
    +242                    md.append("![](../plots/").append(e.getValue().getFileName().toString()).append(")\n\n");
    +243                }
    +244                java.nio.file.Path mdFile = reportsOut.resolve(baseName + ".md");
    +245                java.nio.file.Files.writeString(mdFile, md.toString(), java.nio.charset.StandardCharsets.UTF_8);
    +246
    +247                try {
    +248                    String[] palette = JLineGraph.PALETTE_HEX;
    +249                    java.util.LinkedHashMap<String, java.util.List<Integer>> groupsIdx = new java.util.LinkedHashMap<>();
    +250                    for (int i = 0; i < codes.length; i++) {
    +251                        String code = codes[i];
    +252                        String grp = code != null && code.contains("_") ? code.split("_")[0] : code;
    +253                        groupsIdx.computeIfAbsent(grp, k -> new java.util.ArrayList<>()).add(i);
    +254                    }
    +255                    StringBuilder html = new StringBuilder();
    +256                    html.append("<!doctype html><html><head><meta charset=\"utf-8\"><title>");
    +257                    html.append(this.studentNameParam == null ? "Student Report" : this.studentNameParam).append(" - ").append(dateStr).append("</title>");
    +258                    html.append("<style>body{font-family:sans-serif;margin:20px;} img{max-width:100%;height:auto;border:1px solid #ccc;margin-bottom:8px;} .legend{max-height:160px;overflow:auto;border:1px solid #ddd;padding:8px;margin-bottom:24px;} .legend-item{display:flex;align-items:center;gap:8px;padding:4px 0;} .swatch{width:18px;height:12px;border:1px solid #333;display:inline-block}</style>");
    +259                    html.append("</head><body>");
    +260                    html.append("<h1>").append(this.studentNameParam == null ? "Unknown Student" : this.studentNameParam).append(" - ").append(dateStr).append("</h1>");
    +261                    for (java.util.Map.Entry<String, java.nio.file.Path> e2 : groups.entrySet()) {
    +262                        String grp = e2.getKey();
    +263                        String imgName = e2.getValue().getFileName().toString();
    +264                        html.append("<h2>").append(grp).append("</h2>");
    +265                        html.append("<div class=\"plot\"><img src=\"../plots/").append(imgName).append("\" alt=\"").append(grp).append("\"></div>");
    +266                        java.util.List<Integer> idxs = groupsIdx.getOrDefault(grp, new java.util.ArrayList<>());
    +267                        html.append("<div class=\"legend\">");
    +268                        for (int s = 0; s < idxs.size(); s++) {
    +269                            int idx = idxs.get(s);
    +270                            String code = codes[idx];
    +271                            String human = labels[idx];
    +272                            String seriesName = code + " - " + human;
    +273                            String color = palette[s % palette.length];
    +274                            html.append("<div class=\"legend-item\">");
    +275                            html.append("<span class=\"swatch\" style=\"background:");
    +276                            html.append(color);
    +277                            html.append(";\"></span>");
    +278                            html.append("<div>");
    +279                            html.append(seriesName);
    +280                            html.append("</div></div>");
    +281                        }
    +282                        html.append("</div>");
    +283                    }
    +284                    html.append("</body></html>");
    +285                    java.nio.file.Path htmlFile = reportsOut.resolve(baseName + ".html");
    +286                    java.nio.file.Files.writeString(htmlFile, html.toString(), java.nio.charset.StandardCharsets.UTF_8);
    +287                    LOG.info("Wrote CVI HTML session report {}", htmlFile);
    +288                } catch (java.io.IOException ioex) {
    +289                    LOG.warn("Unable to write CVI HTML report: {}", ioex.toString());
    +290                }
    +291            } catch (java.io.IOException ioe) {
    +292                LOG.warn("Unable to save CVI per-phase plots or markdown report: {}", ioe.toString());
    +293            }
    +294        } catch (SQLException ex) {
    +295            LOG.error("Error saving CVI data", ex);
    +296            com.studentgui.apphelpers.UiNotifier.show("Database error saving CVI data: " + ex.getMessage());
    +297        }
    +298    }
    +299
    +300    // Plotting is handled as part of save(): the submit action updates the shared
    +301    // graph and writes a static PNG into the student's plots folder.
    +302}
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    + + diff --git a/target/site/apidocs/src-html/com/studentgui/apppages/ContactLog.html b/target/site/apidocs/src-html/com/studentgui/apppages/ContactLog.html new file mode 100644 index 0000000..6fb7f0c --- /dev/null +++ b/target/site/apidocs/src-html/com/studentgui/apppages/ContactLog.html @@ -0,0 +1,319 @@ + + + + +Source code + + + + + + +
    +
    +
    001package com.studentgui.apppages;
    +002
    +003import java.awt.BorderLayout;
    +004import java.awt.Font;
    +005import java.awt.GridBagConstraints;
    +006import java.awt.GridBagLayout;
    +007import java.awt.Insets;
    +008import java.awt.event.ActionEvent;
    +009import java.awt.event.KeyEvent;
    +010import java.sql.SQLException;
    +011import java.time.LocalDate;
    +012
    +013import javax.swing.JButton;
    +014import javax.swing.JComboBox;
    +015import javax.swing.JLabel;
    +016import javax.swing.JOptionPane;
    +017import javax.swing.JPanel;
    +018import javax.swing.JScrollPane;
    +019import javax.swing.JTextArea;
    +020import javax.swing.JTextField;
    +021import javax.swing.SwingUtilities;
    +022
    +023import org.slf4j.Logger;
    +024import org.slf4j.LoggerFactory;
    +025
    +026/**
    +027 * Structured parent/guardian contact log with validation and freeform notes.
    +028 *
    +029 * <p>Provides a comprehensive contact tracking form with structured fields for documenting
    +030 * communications with parents, guardians, and family members. Unlike the freeform notes pages
    +031 * ({@link SessionNotes}, {@link Observations}), this page captures both structured metadata
    +032 * and narrative details to support later reporting and documentation requirements.</p>
    +033 *
    +034 * <p><b>Structured Fields:</b></p>
    +035 * <ul>
    +036 *   <li><b>Guardian Name:</b> Full name of the parent/guardian contacted</li>
    +037 *   <li><b>Contact Method:</b> Dropdown selection (Phone, Email, In Person, Other)</li>
    +038 *   <li><b>Phone Number:</b> Contact phone number (validated format: 7-20 chars, digits/+/()-/space)</li>
    +039 *   <li><b>Email Address:</b> Contact email (validated format: basic email regex pattern)</li>
    +040 *   <li><b>Contact Response:</b> Brief summary of the guardian's response or concerns</li>
    +041 *   <li><b>Contact General:</b> High-level topic or category of the contact (e.g., "Progress Update", "IEP Discussion")</li>
    +042 *   <li><b>Contact Specific:</b> Specific items discussed or action points (e.g., "Discussed Braille materials order")</li>
    +043 *   <li><b>Notes:</b> Multi-line freeform notes area for detailed narrative</li>
    +044 * </ul>
    +045 *
    +046 * <p><b>Validation and Error Handling:</b></p>
    +047 * <ul>
    +048 *   <li>Email validation: Triggers warning if Contact Method is "Email" and email field doesn't match {@code ^[^@\s]+@[^@\s]+\.[^@\s]+$}</li>
    +049 *   <li>Phone validation: Triggers warning if Contact Method is "Phone" and phone doesn't match {@code ^[0-9+()\-\s]{7,20}$}</li>
    +050 *   <li>Validation failures display warning dialogs and do not persist data until corrected</li>
    +051 * </ul>
    +052 *
    +053 * <p><b>Data Persistence:</b></p>
    +054 * <ul>
    +055 *   <li>Structured fields persisted via {@link com.studentgui.apphelpers.Database#saveContactLog} to {@code ContactLog} table</li>
    +056 *   <li>Notes also saved to {@code ProgressSession.notes} column via {@link com.studentgui.apphelpers.Database#saveSessionNotes}</li>
    +057 *   <li>JSON export: {@code StudentDataFiles/<student>/Sessions/ContactLog/ContactLog-<sessionId>-<timestamp>.json}</li>
    +058 *   <li>Load Last Contact button retrieves most recent contact record via {@link com.studentgui.apphelpers.Database#fetchLatestContactLog}</li>
    +059 * </ul>
    +060 *
    +061 * <p>No plots are generated (contact logs are non-quantitative). The shared {@link JLineGraph} component
    +062 * is absent from this page's layout. This page does not implement listener interfaces and operates
    +063 * on static student/date parameters.</p>
    +064 *
    +065 * @see com.studentgui.apphelpers.Database#saveContactLog
    +066 * @see com.studentgui.apphelpers.Database#fetchLatestContactLog
    +067 * @see com.studentgui.apphelpers.dto.ContactPayload
    +068 */
    +069public class ContactLog extends JPanel {
    +070    private static final Logger LOG = LoggerFactory.getLogger(ContactLog.class);
    +071    /** Text area where the user enters contact notes for the selected student. */
    +072    private final JTextArea notesArea;
    +073    // additional contact fields
    +074    /** Guardian or parent name associated with the student. */
    +075    private final JTextField guardianField;
    +076    /** Phone number used for contact. */
    +077    private final JTextField phoneField;
    +078    /** Email address used for contact. */
    +079    private final JTextField emailField;
    +080    /** Method of contact (Phone/Email/In Person/Other). */
    +081    private final JComboBox<String> contactMethodCombo;
    +082    /** Short description of the response received during contact. */
    +083    private final JTextField contactResponseField;
    +084    /** High-level/general contact notes (summary). */
    +085    private final JTextField contactGeneralField;
    +086    /** Specific items or action points discussed during contact. */
    +087    private final JTextField contactSpecificField;
    +088
    +089    /** Selected student display name associated with this page instance (may be null). */
    +090    private final String studentNameParam;
    +091
    +092    /** Session date to associate with saved notes from this page. */
    +093    private final LocalDate dateParam;
    +094
    +095    /**
    +096     * Construct a ContactLog page for the provided student and date.
    +097     *
    +098     * @param studentName selected student display name (may be null)
    +099     * @param date session date to associate with saved notes
    +100     * @param graph shared graph component shown under the editor
    +101     */
    +102    public ContactLog(String studentName, LocalDate date, JLineGraph graph) {
    +103    this.studentNameParam = (studentName == null || studentName.trim().isEmpty()) ? com.studentgui.apphelpers.Helpers.defaultStudent() : studentName;
    +104        this.dateParam = date;
    +105        setLayout(new BorderLayout());
    +106
    +107    JPanel p = new JPanel(new GridBagLayout());
    +108    JPanel view = new JPanel(new BorderLayout());
    +109    view.add(p, BorderLayout.NORTH);
    +110    view.setBorder(javax.swing.BorderFactory.createEmptyBorder(20,20,20,20));
    +111    JScrollPane scroll = new JScrollPane(view);
    +112    scroll.getAccessibleContext().setAccessibleName("Contact Log data entry scroll pane");
    +113    GridBagConstraints gbc = new GridBagConstraints(); gbc.insets=new Insets(2,2,2,2); gbc.fill = GridBagConstraints.HORIZONTAL; gbc.anchor = GridBagConstraints.NORTHWEST;
    +114    JLabel title = new JLabel("Contact Log"); title.setFont(title.getFont().deriveFont(Font.BOLD,16)); title.setHorizontalAlignment(JLabel.LEFT); gbc.gridx=0; gbc.gridy=0; gbc.gridwidth=2; p.add(title, gbc);
    +115
    +116    // Structured contact fields (placed above notes)
    +117    int row = 1;
    +118    gbc.gridwidth = 1;
    +119    int globalLabel = com.studentgui.uicomp.PhaseScoreField.getGlobalLabelWidth();
    +120    gbc.gridx = 0; gbc.gridy = row; JLabel guardianLabel = new JLabel("Guardian Name:"); guardianLabel.setPreferredSize(new java.awt.Dimension(globalLabel, guardianLabel.getPreferredSize().height)); p.add(guardianLabel, gbc);
    +121    guardianField = new JTextField(24); guardianField.setName("contactlog_guardian"); gbc.gridx = 1; p.add(guardianField, gbc);
    +122    row++;
    +123    gbc.gridx = 0; gbc.gridy = row; JLabel methodLabel = new JLabel("Contact Method:"); methodLabel.setPreferredSize(new java.awt.Dimension(globalLabel, methodLabel.getPreferredSize().height)); p.add(methodLabel, gbc);
    +124    contactMethodCombo = new JComboBox<>(new String[]{"Phone","Email","In Person","Other"}); contactMethodCombo.setName("contactlog_method"); gbc.gridx = 1; p.add(contactMethodCombo, gbc);
    +125    row++;
    +126    gbc.gridx = 0; gbc.gridy = row; JLabel phoneLabel = new JLabel("Phone Number:"); phoneLabel.setPreferredSize(new java.awt.Dimension(globalLabel, phoneLabel.getPreferredSize().height)); p.add(phoneLabel, gbc);
    +127    phoneField = new JTextField(18); phoneField.setName("contactlog_phone"); gbc.gridx = 1; p.add(phoneField, gbc);
    +128    row++;
    +129    gbc.gridx = 0; gbc.gridy = row; JLabel emailLabel = new JLabel("Email Address:"); emailLabel.setPreferredSize(new java.awt.Dimension(globalLabel, emailLabel.getPreferredSize().height)); p.add(emailLabel, gbc);
    +130    emailField = new JTextField(24); emailField.setName("contactlog_email"); gbc.gridx = 1; p.add(emailField, gbc);
    +131    row++;
    +132    gbc.gridx = 0; gbc.gridy = row; JLabel responseLabel = new JLabel("Contact Response:"); responseLabel.setPreferredSize(new java.awt.Dimension(globalLabel, responseLabel.getPreferredSize().height)); p.add(responseLabel, gbc);
    +133    contactResponseField = new JTextField(24); contactResponseField.setName("contactlog_response"); gbc.gridx = 1; p.add(contactResponseField, gbc);
    +134    row++;
    +135    gbc.gridx = 0; gbc.gridy = row; JLabel generalLabel = new JLabel("Contact General:"); generalLabel.setPreferredSize(new java.awt.Dimension(globalLabel, generalLabel.getPreferredSize().height)); p.add(generalLabel, gbc);
    +136    contactGeneralField = new JTextField(24); contactGeneralField.setName("contactlog_general"); gbc.gridx = 1; p.add(contactGeneralField, gbc);
    +137    row++;
    +138    gbc.gridx = 0; gbc.gridy = row; JLabel specificLabel = new JLabel("Contact Specific:"); specificLabel.setPreferredSize(new java.awt.Dimension(globalLabel, specificLabel.getPreferredSize().height)); p.add(specificLabel, gbc);
    +139    contactSpecificField = new JTextField(24); contactSpecificField.setName("contactlog_specific"); gbc.gridx = 1; p.add(contactSpecificField, gbc);
    +140    row++;
    +141
    +142    // Notes label + text area with accessibility
    +143    gbc.gridx = 0; gbc.gridy = row; gbc.gridwidth = 2; JLabel notesLabel = new JLabel("Notes:"); notesLabel.setPreferredSize(new java.awt.Dimension(globalLabel, notesLabel.getPreferredSize().height)); p.add(notesLabel, gbc);
    +144    row++;
    +145
    +146    gbc.gridx = 0; gbc.gridy = row; gbc.gridwidth = 2; notesArea = new JTextArea(8,40); notesArea.setLineWrap(true); notesArea.setWrapStyleWord(true); notesArea.setToolTipText("Enter contact notes for the student"); notesArea.getAccessibleContext().setAccessibleName("Contact notes"); JScrollPane notesScroll = new JScrollPane(notesArea); notesScroll.setHorizontalScrollBarPolicy(JScrollPane.HORIZONTAL_SCROLLBAR_NEVER); p.add(notesScroll, gbc);
    +147    notesArea.setName("contactlog_notes");
    +148    notesLabel.setLabelFor(notesArea);
    +149
    +150    row++;
    +151    gbc.gridx = 0; gbc.gridy = row; gbc.gridwidth = 1; JButton save = new JButton("Save Contact");
    +152    save.addActionListener((ActionEvent e)-> saveContact());
    +153    save.setToolTipText("Save contact notes to the database");
    +154    save.setMnemonic(KeyEvent.VK_S);
    +155    save.getAccessibleContext().setAccessibleName("Save Contact Notes");
    +156    save.setName("contactlog_save");
    +157    p.add(save, gbc);
    +158
    +159    gbc.gridx = 1; JButton load = new JButton("Load Last Contact");
    +160    load.addActionListener((ActionEvent e) -> loadLastContact());
    +161    load.setToolTipText("Load the most recent contact for the selected student");
    +162    load.setName("contactlog_load");
    +163    p.add(load, gbc);
    +164
    +165    add(scroll, BorderLayout.CENTER);
    +166
    +167        SwingUtilities.invokeLater(()->{ p.setPreferredSize(p.getPreferredSize()); revalidate(); });
    +168
    +169        com.studentgui.apphelpers.Helpers.createFolderHierarchy();
    +170    }
    +171
    +172    private void loadLastContact() {
    +173        try {
    +174            com.studentgui.apphelpers.dto.ContactPayload p = com.studentgui.apphelpers.Database.fetchLatestContactLog(this.studentNameParam);
    +175            if (p == null) {
    +176                com.studentgui.apphelpers.UiNotifier.show("No contact found for this student.");
    +177                return;
    +178            }
    +179            guardianField.setText(p.guardian != null ? p.guardian : "");
    +180            String method = p.method != null ? p.method : "";
    +181            if (method != null) {
    +182                contactMethodCombo.setSelectedItem(method);
    +183            }
    +184            phoneField.setText(p.phone != null ? p.phone : "");
    +185            emailField.setText(p.email != null ? p.email : "");
    +186            contactResponseField.setText(p.response != null ? p.response : "");
    +187            contactGeneralField.setText(p.general != null ? p.general : "");
    +188            contactSpecificField.setText(p.specific != null ? p.specific : "");
    +189            notesArea.setText(p.notes != null ? p.notes : "");
    +190        } catch (SQLException ex) {
    +191            LOG.error("Error loading last contact", ex);
    +192            JOptionPane.showMessageDialog(this, "Database error loading contact: " + ex.getMessage(), "Database error", JOptionPane.ERROR_MESSAGE);
    +193        }
    +194    }
    +195
    +196    /**
    +197     * Persist the contact notes entered into the notes area as a session note
    +198     * for the selected student. Shows a confirmation dialog on success and
    +199     * error dialogs on failure.
    +200     */
    +201    private void saveContact() {
    +202        try {
    +203            int studentId = com.studentgui.apphelpers.Database.getOrCreateStudent(this.studentNameParam);
    +204            int ptId = com.studentgui.apphelpers.Database.getOrCreateProgressType("ContactLog");
    +205            int sessionId = com.studentgui.apphelpers.Database.createProgressSession(studentId, ptId, this.dateParam);
    +206
    +207            String notes = notesArea.getText();
    +208            String guardian = guardianField.getText();
    +209            String method = (String) contactMethodCombo.getSelectedItem();
    +210            String phone = phoneField.getText();
    +211            String email = emailField.getText();
    +212            String response = contactResponseField.getText();
    +213            String general = contactGeneralField.getText();
    +214            String specific = contactSpecificField.getText();
    +215
    +216            // Basic validation
    +217            if (method != null && method.equals("Email") && (email == null || !email.matches("^[^@\\s]+@[^@\\s]+\\.[^@\\s]+$"))) {
    +218                JOptionPane.showMessageDialog(this, "Please enter a valid email address.", "Validation", JOptionPane.WARNING_MESSAGE);
    +219                return;
    +220            }
    +221            if (method != null && method.equals("Phone") && (phone == null || !phone.matches("^[0-9+()\\-\s]{7,20}$"))) {
    +222                JOptionPane.showMessageDialog(this, "Please enter a valid phone number.", "Validation", JOptionPane.WARNING_MESSAGE);
    +223                return;
    +224            }
    +225
    +226            // Save both the free-form notes field on ProgressSession and structured ContactLog row
    +227            com.studentgui.apphelpers.Database.saveSessionNotes(sessionId, notes);
    +228            com.studentgui.apphelpers.Database.saveContactLog(sessionId, this.studentNameParam, this.dateParam.toString(), guardian, method, phone, email, response, general, specific, notes);
    +229            LOG.info("Saved contact log for {}", studentNameParam);
    +230            com.studentgui.apphelpers.UiNotifier.show("Contact log saved.");
    +231            com.studentgui.apphelpers.dto.ContactPayload payload = new com.studentgui.apphelpers.dto.ContactPayload(sessionId, guardian, method, phone, email, response, general, specific, notes);
    +232            java.nio.file.Path jsonOut = com.studentgui.apphelpers.SessionJsonWriter.writeSessionJson(this.studentNameParam, "ContactLog", payload, sessionId);
    +233            if (jsonOut == null) {
    +234                LOG.warn("Unable to save ContactLog session JSON for sessionId={}", sessionId);
    +235            }
    +236        } catch (SQLException ex) {
    +237            LOG.error("Error saving contact log", ex);
    +238            javax.swing.JOptionPane.showMessageDialog(this, "Database error saving contact log: " + ex.getMessage(), "Database error", javax.swing.JOptionPane.ERROR_MESSAGE);
    +239        }
    +240    }
    +241}
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    + + diff --git a/target/site/apidocs/src-html/com/studentgui/apppages/DigitalLiteracy.html b/target/site/apidocs/src-html/com/studentgui/apppages/DigitalLiteracy.html new file mode 100644 index 0000000..39a66e8 --- /dev/null +++ b/target/site/apidocs/src-html/com/studentgui/apppages/DigitalLiteracy.html @@ -0,0 +1,535 @@ + + + + +Source code + + + + + + +
    +
    +
    001package com.studentgui.apppages;
    +002
    +003import java.awt.BorderLayout;
    +004import java.awt.Font;
    +005import java.awt.GridBagConstraints;
    +006import java.awt.GridBagLayout;
    +007import java.awt.Insets;
    +008import java.awt.event.ActionEvent;
    +009import java.awt.event.KeyEvent;
    +010import java.sql.SQLException;
    +011import java.time.LocalDate;
    +012import java.util.List;
    +013
    +014import javax.swing.JButton;
    +015import javax.swing.JLabel;
    +016import javax.swing.JOptionPane;
    +017import javax.swing.JPanel;
    +018import javax.swing.JScrollPane;
    +019import javax.swing.SwingUtilities;
    +020
    +021import org.slf4j.Logger;
    +022import org.slf4j.LoggerFactory;
    +023
    +024/**
    +025 * Digital literacy and computer skills assessment page.
    +026 *
    +027 * <p>Evaluates foundational technology competencies required for academic and professional
    +028 * success in digital environments. Covers 27 skills organized into 5 progressive competency domains:</p>
    +029 *
    +030 * <ul>
    +031 *   <li><b>Phase 1 (P1_1–P1_9): Device Basics and Navigation</b>
    +032 *     <ul>
    +033 *       <li>Powering devices on/off, accessibility feature activation (VoiceOver/TalkBack/Narrator)</li>
    +034 *       <li>Touch/mouse gestures for app launching and navigation</li>
    +035 *       <li>Home screen organization, icon identification, and app launching</li>
    +036 *       <li>Document creation, saving, and retrieval workflows</li>
    +037 *       <li>Online resource access (web portals, learning management systems)</li>
    +038 *       <li>Basic keyboarding (home row, touch typing fundamentals)</li>
    +039 *       <li>UI element interaction (buttons, menus, text fields, sliders)</li>
    +040 *       <li>System-level navigation (Control Center, App Switcher, Task Manager, Dock)</li>
    +041 *     </ul>
    +042 *   </li>
    +043 *   <li><b>Phase 2 (P2_1–P2_6): Word Processing Fundamentals</b>
    +044 *     <ul>
    +045 *       <li>Creating, editing, and saving text documents</li>
    +046 *       <li>Reading and navigating documents using assistive technology or visual scanning</li>
    +047 *       <li>Menu bar and toolbar interaction for formatting and commands</li>
    +048 *       <li>Text selection, highlighting, copy/paste workflows</li>
    +049 *       <li>Image insertion and manipulation (copy, paste, resize, position)</li>
    +050 *       <li>Proofreading strategies and editing for clarity/correctness</li>
    +051 *     </ul>
    +052 *   </li>
    +053 *   <li><b>Phase 3 (P3_1–P3_3): Spreadsheet Fundamentals</b>
    +054 *     <ul>
    +055 *       <li>Describing spreadsheet structure (rows, columns, cells, sheets)</li>
    +056 *       <li>Spreadsheet terminology (cell references, formulas, functions, ranges)</li>
    +057 *       <li>Data entry and editing (typing, autofill, formula entry)</li>
    +058 *     </ul>
    +059 *   </li>
    +060 *   <li><b>Phase 4 (P4_1–P4_5): Presentation Software</b>
    +061 *     <ul>
    +062 *       <li>Presentation tool concepts (slides, layouts, templates)</li>
    +063 *       <li>Creating structured presentations (title, content, transitions)</li>
    +064 *       <li>Editing slides (text, formatting, reordering)</li>
    +065 *       <li>Presenting slides effectively (presenter view, navigation, notes)</li>
    +066 *       <li>Sharing presentations (export, cloud upload, email)</li>
    +067 *     </ul>
    +068 *   </li>
    +069 *   <li><b>Phase 5 (P5_1–P5_5): Digital Citizenship and Ethics</b>
    +070 *     <ul>
    +071 *       <li>Acceptable Use Policies (school/workplace technology guidelines)</li>
    +072 *       <li>Digital citizenship principles (respectful communication, netiquette)</li>
    +073 *       <li>Internet safety (phishing, malware, safe browsing)</li>
    +074 *       <li>Copyright awareness (fair use, attribution, Creative Commons)</li>
    +075 *       <li>Plagiarism recognition and avoidance (paraphrasing, citations, originality)</li>
    +076 *     </ul>
    +077 *   </li>
    +078 * </ul>
    +079 *
    +080 * <p><b>Data Persistence and Report Generation:</b></p>
    +081 * <ul>
    +082 *   <li>Scores captured via {@link com.studentgui.uicomp.PhaseScoreField} (integer 0–4 typical)</li>
    +083 *   <li>Persisted to normalized schema via {@link com.studentgui.apphelpers.Database#insertAssessmentResults}</li>
    +084 *   <li>JSON export: {@code StudentDataFiles/<student>/Sessions/DigitalLiteracy/DigitalLiteracy-<sessionId>-<timestamp>.json}</li>
    +085 *   <li>Phase-grouped time-series plots: {@code plots/DigitalLiteracy-<sessionId>-<date>-P<N>.png} (5 phase groups)</li>
    +086 *   <li>Markdown and HTML reports with embedded plots and color-coded legends</li>
    +087 * </ul>
    +088 *
    +089 * <p>The shared {@link JLineGraph} visualizes recent session trends grouped by phase prefix.
    +090 * Implements {@link com.studentgui.app.DateChangeListener} and {@link com.studentgui.app.StudentChangeListener}
    +091 * for dynamic updates when global selections change.</p>
    +092 *
    +093 * <p><b>Note:</b> Skill codes and phases intentionally overlap with {@link IOS} to allow
    +094 * cross-platform skill mapping. Some assessment items are device-agnostic and track the same
    +095 * underlying competencies across iOS, Windows, macOS, and ChromeOS environments.</p>
    +096 *
    +097 * @see com.studentgui.apphelpers.Database
    +098 * @see JLineGraph
    +099 * @see com.studentgui.uicomp.PhaseScoreField
    +100 * @see IOS
    +101 */
    +102public class DigitalLiteracy extends JPanel implements com.studentgui.app.DateChangeListener, com.studentgui.app.StudentChangeListener {
    +103    private static final Logger LOG = LoggerFactory.getLogger(DigitalLiteracy.class);
    +104    /** Array of input fields for each digital literacy skill part. */
    +105    private final com.studentgui.uicomp.PhaseScoreField[] skillFields;
    +106    /** Canonical list of digital literacy assessment parts: code and display label. */
    +107    private final String[][] parts;
    +108
    +109    /** Shared graph used to visualize recent digital literacy sessions. */
    +110    private final JLineGraph lineGraph; // Reference to the JLineGraph instance
    +111
    +112    /** Selected student's display name (may be null) for saving/fetching data. */
    +113    private String studentNameParam;
    +114    /** Title label shown at the top of the Digital Literacy page. */
    +115    private JLabel titleLabel;
    +116    /** Base title text for the page; used when building the header string. */
    +117    private final String baseTitle = "Digital Literacy Skills Progression";
    +118
    +119    /** Session date to associate with persisted digital literacy progress. */
    +120    private LocalDate dateParam;
    +121
    +122    /**
    +123     * Construct the Digital Literacy page for the given student and date.
    +124     *
    +125     * @param studentName display name of the selected student (may be null)
    +126     * @param date session date to associate with persisted progress
    +127     * @param lineGraph shared graph component used to display recent results
    +128     */
    +129    public DigitalLiteracy(final String studentName, final LocalDate date, final JLineGraph lineGraph) {
    +130    this.studentNameParam = (studentName == null || studentName.trim().isEmpty()) ? com.studentgui.apphelpers.Helpers.defaultStudent() : studentName;
    +131        this.dateParam = date;
    +132        this.lineGraph = lineGraph; // Use the passed in graph instance
    +133        setLayout(new BorderLayout());
    +134
    +135    this.parts = new String[][]{
    +136            {"P1_1","1.1 Turn Device On/Off"},{"P1_2","1.2 Turn VoiceOver On/Off"},{"P1_3","1.3 Gestures to Click Icons"},{"P1_4","1.4 Home Screen Icons to Open Documents"},{"P1_5","1.5 Save Documents"},{"P1_6","1.6 Online Tools/Resources"},{"P1_7","1.7 Keyboarding"},{"P1_8","1.8 Use Different Elements"},{"P1_9","1.9 Control Center, App Switcher..."},
    +137            {"P2_1","2.1 Write, edit save"},{"P2_2","2.2 Read, Navigate Document"},{"P2_3","2.3 Use Menubar"},{"P2_4","2.4 Highlight text, copy and paste text"},{"P2_5","2.5 Copy and paste images"},{"P2_6","2.6 Proofread and edit"},
    +138            {"P3_1","3.1 Describe Spreadsheet"},{"P3_2","3.2 Explain terms and concepts"},{"P3_3","3.3 Enter/Edit data"},
    +139            {"P4_1","4.1 Presentation Tools"},{"P4_2","4.2 Create Slides"},{"P4_3","4.3 Edit Slides"},{"P4_4","4.4 Present Slides"},{"P4_5","4.5 Share Slides"},
    +140            {"P5_1","5.1 Acceptable Use"},{"P5_2","5.2 Digital Citizenship"},{"P5_3","5.3 Internet Safety"},{"P5_4","5.4 Copyright"},{"P5_5","5.5 Plagiarism"}
    +141        };
    +142
    +143        // Panel for data entry
    +144        JPanel dataEntryPanel = new JPanel();
    +145        dataEntryPanel.setLayout(new GridBagLayout());
    +146        JScrollPane dataEntryScrollPane = new JScrollPane(dataEntryPanel);
    +147        dataEntryScrollPane.setVerticalScrollBarPolicy(JScrollPane.VERTICAL_SCROLLBAR_AS_NEEDED);
    +148        dataEntryScrollPane.setHorizontalScrollBarPolicy(JScrollPane.HORIZONTAL_SCROLLBAR_NEVER);
    +149
    +150        GridBagConstraints gbc = new GridBagConstraints();
    +151            gbc.insets = new Insets(2, 2, 2, 2);
    +152        gbc.fill = GridBagConstraints.HORIZONTAL;
    +153        gbc.weightx = 1.0;
    +154        gbc.weighty = 0.0;
    +155
    +156    this.titleLabel = new JLabel(baseTitle);
    +157        this.titleLabel.setFont(this.titleLabel.getFont().deriveFont(Font.BOLD, 16));
    +158        gbc.gridx = 0;
    +159        gbc.gridy = 0;
    +160        gbc.gridwidth = GridBagConstraints.REMAINDER;
    +161        dataEntryPanel.add(this.titleLabel, gbc);
    +162
    +163        gbc.gridy = 1;
    +164        gbc.gridwidth = GridBagConstraints.REMAINDER;
    +165        gbc.ipady = 20;
    +166        dataEntryPanel.add(new JPanel(), gbc);
    +167
    +168    // layout spacing handled by PhaseScoreField
    +169
    +170        String[] labels = java.util.Arrays.stream(parts).map(x->x[1]).toArray(String[]::new);
    +171            int maxPx = com.studentgui.uicomp.PhaseScoreField.computeMaxLabelPixelWidth(titleLabel.getFont(), labels);
    +172            com.studentgui.uicomp.PhaseScoreField.setGlobalLabelWidth(Math.min(320, Math.max(140, maxPx + 50)));
    +173    skillFields = new com.studentgui.uicomp.PhaseScoreField[this.parts.length];
    +174    for (int i = 0; i < this.parts.length; i++) {
    +175            gbc.gridy = i + 2;
    +176            gbc.gridx = 0;
    +177            gbc.gridwidth = 1;
    +178            com.studentgui.uicomp.PhaseScoreField field = new com.studentgui.uicomp.PhaseScoreField(parts[i][1], 0);
    +179            field.setName("digitalliteracy_" + this.parts[i][0]);
    +180            field.getAccessibleContext().setAccessibleName(this.parts[i][1]);
    +181            field.setToolTipText("Enter whole number score for " + this.parts[i][1]);
    +182            gbc.gridx = 0; gbc.gridwidth = 2; gbc.insets = new Insets(5, 5, 5, 5);
    +183            dataEntryPanel.add(field, gbc);
    +184            skillFields[i] = field;
    +185            gbc.gridx = 2; gbc.gridwidth = 1; gbc.insets = new Insets(5, 0, 5, 5);
    +186            dataEntryPanel.add(new JPanel(), gbc);
    +187        }
    +188
    +189    gbc.gridy = this.parts.length + 3;
    +190        gbc.gridx = 0;
    +191        gbc.gridwidth = GridBagConstraints.REMAINDER;
    +192        gbc.weighty = 1.0;
    +193        dataEntryPanel.add(new JPanel(), gbc);
    +194
    +195    // Place Submit and Open Latest side-by-side and match IOS button height
    +196    gbc.gridy = this.parts.length + 4;
    +197    gbc.weighty = 0.0;
    +198    gbc.gridx = 0;
    +199    gbc.gridwidth = 1;
    +200    JButton submitDataButton = new JButton("Submit Data");
    +201    submitDataButton.setPreferredSize(new java.awt.Dimension(0, 32));
    +202    submitDataButton.addActionListener((ActionEvent e) -> { submitData(); refreshGraph(); });
    +203    submitDataButton.setToolTipText("Save digital literacy scores for the selected student (Alt+S)");
    +204    submitDataButton.setMnemonic(KeyEvent.VK_S);
    +205    submitDataButton.getAccessibleContext().setAccessibleName("Submit Digital Literacy Data");
    +206    submitDataButton.setName("digitalliteracy_submit");
    +207    dataEntryPanel.add(submitDataButton, gbc);
    +208
    +209    gbc.gridx = 1;
    +210    JButton openLatestBtn = new JButton("Open Latest Plot");
    +211    openLatestBtn.setPreferredSize(new java.awt.Dimension(0, 32));
    +212    openLatestBtn.addActionListener((ActionEvent e) -> {
    +213        java.nio.file.Path p = com.studentgui.apphelpers.Helpers.latestPlotPath(this.studentNameParam, "DigitalLiteracy");
    +214        if (p == null) {
    +215            com.studentgui.apphelpers.UiNotifier.show("No DigitalLiteracy plot found for student");
    +216        } else {
    +217            try {
    +218                java.awt.Desktop.getDesktop().open(p.toFile());
    +219            } catch (java.io.IOException | UnsupportedOperationException | SecurityException ex) {
    +220                com.studentgui.apphelpers.UiNotifier.show("Unable to open plot: " + p.getFileName().toString());
    +221            }
    +222        }
    +223    });
    +224    dataEntryPanel.add(openLatestBtn, gbc);
    +225
    +226    gbc.gridx = 2; gbc.gridwidth = GridBagConstraints.REMAINDER;
    +227    dataEntryPanel.add(new JPanel(), gbc);
    +228
    +229    dataEntryScrollPane.getAccessibleContext().setAccessibleName("Digital Literacy data entry scroll pane");
    +230
    +231        add(dataEntryScrollPane, BorderLayout.CENTER);
    +232
    +233        // Add existing graph reference
    +234        add(lineGraph, BorderLayout.SOUTH);
    +235
    +236        SwingUtilities.invokeLater(() -> {
    +237            dataEntryPanel.setPreferredSize(dataEntryPanel.getPreferredSize());
    +238            updateTitleDate();
    +239            revalidate();
    +240        });
    +241
    +242        // Ensure application folders and DB schema exist
    +243        com.studentgui.apphelpers.Helpers.createFolderHierarchy();
    +244        initDatabase();
    +245        refreshGraph();
    +246    }
    +247
    +248    /**
    +249     * Ensure the progress type and assessment parts for DigitalLiteracy exist
    +250     * in the canonical schema.
    +251     */
    +252    private void initDatabase() {
    +253        try {
    +254            int ptId = com.studentgui.apphelpers.Database.getOrCreateProgressType("DigitalLiteracy");
    +255            // Use canonical part codes from this.parts
    +256            String[] codes = new String[this.parts.length];
    +257            for (int i = 0; i < this.parts.length; i++) {
    +258                codes[i] = this.parts[i][0];
    +259            }
    +260            com.studentgui.apphelpers.Database.ensureAssessmentParts(ptId, codes);
    +261        } catch (SQLException e) {
    +262            LOG.error("SQL error ensuring assessment parts for DigitalLiteracy", e);
    +263        }
    +264    }
    +265
    +266    /**
    +267     * Validate and persist input field values as a new progress session for
    +268     * the selected student.
    +269     */
    +270    private void submitData() {
    +271        if (studentNameParam == null || studentNameParam.trim().isEmpty()) {
    +272            JOptionPane.showMessageDialog(this, "Please select a student before submitting Digital Literacy data.", "Missing student", JOptionPane.WARNING_MESSAGE);
    +273            return;
    +274        }
    +275
    +276        try {
    +277            int studentId = com.studentgui.apphelpers.Database.getOrCreateStudent(studentNameParam);
    +278            int ptId = com.studentgui.apphelpers.Database.getOrCreateProgressType("DigitalLiteracy");
    +279            int sessionId = com.studentgui.apphelpers.Database.createProgressSession(studentId, ptId, dateParam);
    +280
    +281            String[] codes = new String[this.parts.length];
    +282            int[] scores = new int[this.parts.length];
    +283            for (int i = 0; i < this.parts.length; i++) {
    +284                codes[i] = this.parts[i][0];
    +285                scores[i] = skillFields[i].getValue();
    +286            }
    +287            com.studentgui.apphelpers.Database.insertAssessmentResults(sessionId, ptId, codes, scores);
    +288            LOG.info("Data submitted successfully via normalized schema.");
    +289            com.studentgui.apphelpers.UiNotifier.show("Digital Literacy data saved.");
    +290            com.studentgui.apphelpers.dto.AssessmentPayload payload = new com.studentgui.apphelpers.dto.AssessmentPayload(sessionId, codes, scores);
    +291            java.nio.file.Path jsonOut = com.studentgui.apphelpers.SessionJsonWriter.writeSessionJson(this.studentNameParam, "DigitalLiteracy", payload, sessionId);
    +292            if (jsonOut == null) {
    +293                LOG.warn("Unable to save DigitalLiteracy session JSON for sessionId={}", sessionId);
    +294            }
    +295            try {
    +296                java.nio.file.Path plotsOut = com.studentgui.apphelpers.Helpers.studentPlotsDir(this.studentNameParam);
    +297                java.nio.file.Path reportsOut = com.studentgui.apphelpers.Helpers.studentReportsDir(this.studentNameParam);
    +298                java.nio.file.Files.createDirectories(plotsOut);
    +299                java.nio.file.Files.createDirectories(reportsOut);
    +300                java.time.format.DateTimeFormatter df = java.time.format.DateTimeFormatter.ISO_DATE;
    +301                String dateStr = this.dateParam != null ? this.dateParam.format(df) : java.time.LocalDate.now().toString();
    +302                String baseName = "DigitalLiteracy-" + sessionId + "-" + dateStr;
    +303
    +304                com.studentgui.apphelpers.Database.ResultsWithDates rwd = com.studentgui.apphelpers.Database.fetchLatestAssessmentResultsWithDates(this.studentNameParam, "DigitalLiteracy", Integer.MAX_VALUE);
    +305                java.util.Map<String, java.nio.file.Path> groups = null;
    +306                String[] labels = new String[this.parts.length];
    +307                for (int i = 0; i < this.parts.length; i++) {
    +308                    labels[i] = this.parts[i][1];
    +309                }
    +310                if (rwd != null && rwd.rows != null && !rwd.rows.isEmpty()) {
    +311                    lineGraph.updateWithGroupedDataByDate(rwd.dates, rwd.rows, codes, labels);
    +312                    groups = lineGraph.saveGroupedCharts(plotsOut, baseName, 1000, 240);
    +313                    java.time.LocalDate headerDate = rwd.dates.get(rwd.dates.size() - 1);
    +314                    dateStr = headerDate.format(df);
    +315                } else {
    +316                    java.util.List<java.util.List<Integer>> rowsList = new java.util.ArrayList<>();
    +317                    java.util.List<Integer> latest = new java.util.ArrayList<>();
    +318                    for (int v : scores) latest.add(v);
    +319                    rowsList.add(latest);
    +320                    lineGraph.updateWithGroupedData(rowsList, codes);
    +321                    groups = lineGraph.saveGroupedCharts(plotsOut, baseName, 1000, 240);
    +322                }
    +323
    +324                if (groups == null) {
    +325                    groups = new java.util.LinkedHashMap<>();
    +326                }
    +327                StringBuilder md = new StringBuilder();
    +328                md.append("# ").append(this.studentNameParam == null ? "Unknown Student" : this.studentNameParam).append(" - ").append(dateStr).append("\n\n");
    +329                for (java.util.Map.Entry<String, java.nio.file.Path> e : groups.entrySet()) {
    +330                    md.append("## ").append(e.getKey()).append("\n\n");
    +331                    md.append("![](../plots/").append(e.getValue().getFileName().toString()).append(")\n\n");
    +332                }
    +333                java.nio.file.Path mdFile = reportsOut.resolve(baseName + ".md");
    +334                java.nio.file.Files.writeString(mdFile, md.toString(), java.nio.charset.StandardCharsets.UTF_8);
    +335
    +336                try {
    +337                    String[] palette = JLineGraph.PALETTE_HEX;
    +338                    java.util.LinkedHashMap<String, java.util.List<Integer>> groupsIdx = new java.util.LinkedHashMap<>();
    +339                    for (int i = 0; i < codes.length; i++) {
    +340                        String code = codes[i];
    +341                        String grp = code != null && code.contains("_") ? code.split("_")[0] : code;
    +342                        groupsIdx.computeIfAbsent(grp, k -> new java.util.ArrayList<>()).add(i);
    +343                    }
    +344                    StringBuilder html = new StringBuilder();
    +345                    html.append("<!doctype html><html><head><meta charset=\"utf-8\"><title>");
    +346                    html.append(this.studentNameParam == null ? "Student Report" : this.studentNameParam).append(" - ").append(dateStr).append("</title>");
    +347                    html.append("<style>body{font-family:sans-serif;margin:20px;} img{max-width:100%;height:auto;border:1px solid #ccc;margin-bottom:8px;} .legend{max-height:160px;overflow:auto;border:1px solid #ddd;padding:8px;margin-bottom:24px;} .legend-item{display:flex;align-items:center;gap:8px;padding:4px 0;} .swatch{width:18px;height:12px;border:1px solid #333;display:inline-block}</style>");
    +348                    html.append("</head><body>");
    +349                    html.append("<h1>").append(this.studentNameParam == null ? "Unknown Student" : this.studentNameParam).append(" - ").append(dateStr).append("</h1>");
    +350                    for (java.util.Map.Entry<String, java.nio.file.Path> e2 : groups.entrySet()) {
    +351                        String grp = e2.getKey();
    +352                        String imgName = e2.getValue().getFileName().toString();
    +353                        html.append("<h2>").append(grp).append("</h2>");
    +354                        html.append("<div class=\"plot\"><img src=\"../plots/").append(imgName).append("\" alt=\"").append(grp).append("\"></div>");
    +355                        java.util.List<Integer> idxs = groupsIdx.getOrDefault(grp, new java.util.ArrayList<>());
    +356                        html.append("<div class=\"legend\">");
    +357                        for (int s = 0; s < idxs.size(); s++) {
    +358                            int idx = idxs.get(s);
    +359                            String code = codes[idx];
    +360                            String human = this.parts[idx][1];
    +361                            String seriesName = code + " - " + human;
    +362                            String color = palette[s % palette.length];
    +363                            html.append("<div class=\"legend-item\">");
    +364                            html.append("<span class=\"swatch\" style=\"background:");
    +365                            html.append(color);
    +366                            html.append(";\"></span>");
    +367                            html.append("<div>");
    +368                            html.append(seriesName);
    +369                            html.append("</div></div>");
    +370                        }
    +371                        html.append("</div>");
    +372                    }
    +373                    html.append("</body></html>");
    +374                    java.nio.file.Path htmlFile = reportsOut.resolve(baseName + ".html");
    +375                    java.nio.file.Files.writeString(htmlFile, html.toString(), java.nio.charset.StandardCharsets.UTF_8);
    +376                    LOG.info("Wrote DigitalLiteracy HTML session report {}", htmlFile);
    +377                } catch (java.io.IOException ioex) {
    +378                    LOG.warn("Unable to write DigitalLiteracy HTML report: {}", ioex.toString());
    +379                }
    +380            } catch (java.io.IOException ioe) {
    +381                LOG.warn("Unable to save DigitalLiteracy per-phase plots or markdown report: {}", ioe.toString());
    +382            }
    +383        } catch (SQLException e) {
    +384            LOG.error("SQL error submitting Digital Literacy data", e);
    +385            JOptionPane.showMessageDialog(this, "Database error saving Digital Literacy data: " + e.getMessage(), "Database error", JOptionPane.ERROR_MESSAGE);
    +386        }
    +387    }
    +388
    +389    /**
    +390     * Load recent assessment sessions and update the shared {@link JLineGraph}
    +391     * component with the returned values.
    +392     */
    +393    private void refreshGraph() {
    +394        try {
    +395            List<List<Integer>> allSkillValues = com.studentgui.apphelpers.Database.fetchLatestAssessmentResults(studentNameParam, "DigitalLiteracy", 5);
    +396            if (allSkillValues != null && !allSkillValues.isEmpty()) {
    +397                // Build canonical codes array in the same order used when ensuring parts
    +398                String[] codes = new String[this.parts.length];
    +399                for (int i = 0; i < this.parts.length; i++) {
    +400                    codes[i] = this.parts[i][0];
    +401                }
    +402                    lineGraph.updateWithGroupedData(allSkillValues, codes);
    +403                    // Write to the consolidated per-run data dumps file when enabled
    +404                    if (Boolean.parseBoolean(com.studentgui.apphelpers.Settings.get("dump.enabled", "false"))) {
    +405                        try {
    +406                            String appHome = System.getProperty("APP_HOME", com.studentgui.apphelpers.Helpers.APP_HOME.toString());
    +407                            String ts = System.getProperty("LOG_TS", String.valueOf(java.time.Instant.now().getEpochSecond()));
    +408                            java.nio.file.Path logDir = java.nio.file.Paths.get(appHome).resolve("logs");
    +409                            java.nio.file.Files.createDirectories(logDir);
    +410                            java.nio.file.Path logFile = logDir.resolve("data_dumps_" + ts + ".log");
    +411                            StringBuilder sb = new StringBuilder();
    +412                            sb.append("[DigitalLiteracy]").append(System.lineSeparator());
    +413                            sb.append(java.time.Instant.now().toString()).append(" - student=").append(this.studentNameParam).append(System.lineSeparator());
    +414                            sb.append("data=").append(allSkillValues.toString()).append(System.lineSeparator());
    +415                            sb.append(System.lineSeparator());
    +416                            java.nio.file.Files.writeString(logFile, sb.toString(), java.nio.charset.StandardCharsets.UTF_8, java.nio.file.StandardOpenOption.CREATE, java.nio.file.StandardOpenOption.APPEND);
    +417                        } catch (java.io.IOException ioe) {
    +418                            LOG.trace("Unable to write DigitalLiteracy load log: {}", ioe.toString());
    +419                        }
    +420                    }
    +421            } else {
    +422                LOG.info("No data to plot.");
    +423            }
    +424        } catch (SQLException e) {
    +425            LOG.error("SQL error refreshing Digital Literacy graph", e);
    +426        }
    +427    }
    +428
    +429    @Override
    +430    public void dateChanged(final LocalDate newDate) {
    +431        this.dateParam = newDate;
    +432        SwingUtilities.invokeLater(() -> {
    +433            refreshGraph();
    +434            updateTitleDate();
    +435        });
    +436    }
    +437
    +438    @Override
    +439    public void studentChanged(final String newStudent) {
    +440        this.studentNameParam = newStudent;
    +441        SwingUtilities.invokeLater(() -> {
    +442            refreshGraph();
    +443            updateTitleDate();
    +444        });
    +445    }
    +446
    +447    private void updateTitleDate() {
    +448        try {
    +449            String dateStr = this.dateParam != null ? this.dateParam.toString() : java.time.LocalDate.now().toString();
    +450            this.titleLabel.setText(baseTitle + " - " + dateStr);
    +451        } catch (Exception ex) {
    +452            this.titleLabel.setText(baseTitle);
    +453        }
    +454    }
    +455    
    +456
    +457}
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    + + diff --git a/target/site/apidocs/src-html/com/studentgui/apppages/Homepage.html b/target/site/apidocs/src-html/com/studentgui/apppages/Homepage.html new file mode 100644 index 0000000..7a1a292 --- /dev/null +++ b/target/site/apidocs/src-html/com/studentgui/apppages/Homepage.html @@ -0,0 +1,153 @@ + + + + +Source code + + + + + + +
    +
    +
    001package com.studentgui.apppages;
    +002
    +003import java.awt.BorderLayout;
    +004
    +005import javax.swing.JLabel;
    +006import javax.swing.JPanel;
    +007import javax.swing.JScrollPane;
    +008import javax.swing.JTextArea;
    +009import javax.swing.SwingConstants;
    +010
    +011/**
    +012 * Simple homepage panel with application overview/help text.
    +013 *
    +014 * <p>Provides a small, static help and overview text area that can be
    +015 * embedded into the main application frame.</p>
    +016 */
    +017public class Homepage {
    +018    /**
    +019     * Create the homepage panel which contains a title and an overview/help
    +020     * text area.
    +021     *
    +022     * @return a ready-to-add {@link JPanel} containing the application overview
    +023     */
    +024    public static JPanel create() {
    +025        JPanel p = new JPanel(new BorderLayout());
    +026    JLabel title = new JLabel("Student Skills Progressions", SwingConstants.LEFT);
    +027        title.setFont(title.getFont().deriveFont(24f));
    +028        title.getAccessibleContext().setAccessibleName("Student Skills Progressions title");
    +029        title.setName("homepage_title");
    +030        p.add(title, BorderLayout.NORTH);
    +031
    +032    JTextArea body = new JTextArea();
    +033                body.setLineWrap(true);
    +034                body.setWrapStyleWord(true);
    +035                String text = """
    +036                                Welcome to the Student Skills Progressions application.
    +037
    +038                                This tool helps educators track and record student progress across a set of vision and access skill areas (Braille, Abacus, Digital Literacy, iOS access, Screen Reader, CVI, Keyboarding, and more).
    +039
    +040                                How to use:
    +041                                    1. Select a student from the Student dropdown at the top-left.
    +042                                    2. Use the Date field to set the session date and click Apply to recreate pages for that date.
    +043                                    3. Navigate to a skill page using the Navigate menu (or the top control bar). Each skill page contains standardized rows for entering phase/score values.
    +044                                    4. Enter assessment data and notes on each page. Use the Save / Submit buttons on pages where available to persist data to the local SQLite database.
    +045                                    5. The shared graph shows progress trends for the selected student. Session notes and contact logs provide a place for free-form observations and structured contact records.
    +046
    +047                                Data storage and export:
    +048                                    • All data is stored locally in a SQLite database under the application data folder.
    +049                                    • Use the Instructional Materials page to open and manage student-facing materials and reports.
    +050
    +051                                Support and workflow tips:
    +052                                    • Start each session by verifying the student and date, then move through skill pages, entering scores and notes.
    +053                                    • Use Contact Log to record family/guardian contact; structured fields make later reporting easier.
    +054                                    • If you need to reset or recreate pages for a student/date, use the Apply button after changing the date.
    +055
    +056                                Thanks for using the Student Skills Progressions application.
    +057                                """;
    +058                body.setText(text);
    +059        body.setEditable(false);
    +060        body.setToolTipText("Overview and quick help about the application");
    +061        body.getAccessibleContext().setAccessibleName("Homepage overview");
    +062        JScrollPane bodyScroll = new JScrollPane(body);
    +063        bodyScroll.getAccessibleContext().setAccessibleName("Homepage overview scroll pane");
    +064        body.setName("homepage_body");
    +065        p.add(bodyScroll, BorderLayout.CENTER);
    +066        return p;
    +067    }
    +068
    +069    /**
    +070     * Private constructor to prevent instantiation of this utility class.
    +071     */
    +072    private Homepage() {
    +073        throw new AssertionError("Not instantiable");
    +074    }
    +075}
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    + + diff --git a/target/site/apidocs/src-html/com/studentgui/apppages/IOS.html b/target/site/apidocs/src-html/com/studentgui/apppages/IOS.html new file mode 100644 index 0000000..397ed05 --- /dev/null +++ b/target/site/apidocs/src-html/com/studentgui/apppages/IOS.html @@ -0,0 +1,473 @@ + + + + +Source code + + + + + + +
    +
    +
    001package com.studentgui.apppages;
    +002
    +003import java.awt.BorderLayout;
    +004import java.awt.Font;
    +005import java.awt.GridBagConstraints;
    +006import java.awt.GridBagLayout;
    +007import java.awt.Insets;
    +008import java.awt.event.ActionEvent;
    +009import java.awt.event.KeyEvent;
    +010import java.sql.SQLException;
    +011import java.time.LocalDate;
    +012import java.util.LinkedHashMap;
    +013import java.util.Map;
    +014
    +015import javax.swing.JButton;
    +016import javax.swing.JLabel;
    +017import javax.swing.JPanel;
    +018import javax.swing.JScrollPane;
    +019import javax.swing.SwingUtilities;
    +020
    +021import org.slf4j.Logger;
    +022import org.slf4j.LoggerFactory;
    +023
    +024import com.studentgui.uicomp.PhaseScoreField;
    +025
    +026/**
    +027 * iOS and iPadOS assistive technology proficiency assessment page.
    +028 *
    +029 * <p>Provides structured evaluation of iOS/iPadOS device usage skills across
    +030 * 41 competencies organized into 6 functional domains:</p>
    +031 *
    +032 * <ul>
    +033 *   <li><b>Phase 1 (P1_1–P1_9): Device Basics and VoiceOver Fundamentals</b>
    +034 *     <ul>
    +035 *       <li>Power management, VoiceOver activation/deactivation</li>
    +036 *       <li>Core gestures (tap, swipe, rotor) for icon navigation and interaction</li>
    +037 *       <li>Home screen management, document handling, keyboarding basics</li>
    +038 *       <li>Control Center, App Switcher, and system-level navigation</li>
    +039 *     </ul>
    +040 *   </li>
    +041 *   <li><b>Phase 2 (P2_1–P2_6): Word Processing and Document Creation</b>
    +042 *     <ul>
    +043 *       <li>Creating, editing, and saving text documents</li>
    +044 *       <li>Reading and navigating within documents using VoiceOver</li>
    +045 *       <li>Menu bar interaction, text/image copy-paste workflows</li>
    +046 *       <li>Proofreading and editing strategies with assistive technology</li>
    +047 *     </ul>
    +048 *   </li>
    +049 *   <li><b>Phase 3 (P3_1–P3_5): Spreadsheet and Data Visualization</b>
    +050 *     <ul>
    +051 *       <li>Spreadsheet concepts and terminology (rows, columns, cells, formulas)</li>
    +052 *       <li>Data entry, editing, and spreadsheet navigation with VoiceOver</li>
    +053 *       <li>Creating and interpreting charts/graphs from data</li>
    +054 *     </ul>
    +055 *   </li>
    +056 *   <li><b>Phase 4 (P4_1–P4_5): Presentation Software</b>
    +057 *     <ul>
    +058 *       <li>Creating and structuring presentations with accessible workflows</li>
    +059 *       <li>Editing slides, adding multimedia content (images, audio)</li>
    +060 *       <li>Presenting slides effectively using assistive technology</li>
    +061 *       <li>Sharing and exporting presentations</li>
    +062 *     </ul>
    +063 *   </li>
    +064 *   <li><b>Phase 5 (P5_1–P5_7): Digital Citizenship and Online Safety</b>
    +065 *     <ul>
    +066 *       <li>Acceptable Use Policies, digital citizenship principles</li>
    +067 *       <li>Online safety, privacy awareness, copyright/plagiarism concepts</li>
    +068 *       <li>Recognizing and responding to cyberbullying</li>
    +069 *     </ul>
    +070 *   </li>
    +071 *   <li><b>Phase 6 (P6_1–P6_11): Device Management and Connectivity</b>
    +072 *     <ul>
    +073 *       <li>App installation, updates, deletion, storage management</li>
    +074 *       <li>Accessibility settings configuration and customization</li>
    +075 *       <li>Screen Time controls, Parental Controls</li>
    +076 *       <li>Connectivity features: Bluetooth, Wi-Fi, AirDrop, Personal Hotspot</li>
    +077 *     </ul>
    +078 *   </li>
    +079 * </ul>
    +080 *
    +081 * <p><b>Data Management and Artifacts:</b></p>
    +082 * <ul>
    +083 *   <li>Scores captured via {@link PhaseScoreField} components (typically 0–4 integer range)</li>
    +084 *   <li>Persisted to normalized schema using {@link com.studentgui.apphelpers.Database#insertAssessmentResults}</li>
    +085 *   <li>JSON session export: {@code StudentDataFiles/<student>/Sessions/iOS/iOS-<sessionId>-<timestamp>.json}</li>
    +086 *   <li>Phase-grouped time-series PNG plots saved to {@code plots/} directory</li>
    +087 *   <li>Markdown and HTML reports generated with embedded plots and color-coded legends</li>
    +088 * </ul>
    +089 *
    +090 * <p>The shared {@link JLineGraph} visualizes recent session trends grouped by phase prefix
    +091 * to maintain chart readability. This page operates on static student/date parameters and
    +092 * does not implement listener interfaces for dynamic updates.</p>
    +093 *
    +094 * @see com.studentgui.apphelpers.Database
    +095 * @see JLineGraph
    +096 * @see PhaseScoreField
    +097 */
    +098public class IOS extends JPanel {
    +099    private static final Logger LOG = LoggerFactory.getLogger(IOS.class);
    +100    /** Mapping of iOS assessment part codes to their input components. */
    +101    private final Map<String, PhaseScoreField> inputs = new LinkedHashMap<>();
    +102
    +103    /** Selected student display name used for saves and plots (may be null). */
    +104    private final String studentNameParam;
    +105
    +106    /** Session date to associate with saved iOS progress entries. */
    +107    private final LocalDate dateParam;
    +108
    +109    /** Shared graph component for plotting recent iOS assessment sessions. */
    +110    private final JLineGraph graph;
    +111
    +112    /**
    +113     * Construct the iOS page for the given student and date.
    +114     *
    +115     * @param studentName selected student name (may be null)
    +116     * @param date session date to associate with saved progress
    +117     * @param graph shared graph used to visualize recent sessions
    +118     */
    +119    public IOS(String studentName, LocalDate date, JLineGraph graph) {
    +120    this.studentNameParam = (studentName == null || studentName.trim().isEmpty()) ? com.studentgui.apphelpers.Helpers.defaultStudent() : studentName;
    +121        this.dateParam = date;
    +122        this.graph = graph;
    +123        setLayout(new BorderLayout());
    +124
    +125    JPanel p = new JPanel(new GridBagLayout());
    +126    JPanel view = new JPanel(new BorderLayout());
    +127    view.add(p, BorderLayout.NORTH);
    +128    view.setBorder(javax.swing.BorderFactory.createEmptyBorder(20,20,20,20));
    +129    JScrollPane scroll = new JScrollPane(view);
    +130    scroll.getAccessibleContext().setAccessibleName("iOS data entry scroll pane");
    +131        GridBagConstraints gbc = new GridBagConstraints(); gbc.insets=new Insets(2,2,2,2); gbc.fill = GridBagConstraints.HORIZONTAL; gbc.anchor = GridBagConstraints.NORTHWEST; gbc.weightx = 1.0;
    +132
    +133    JLabel title = new JLabel("iOS / iPad OS Skills");
    +134    title.setFont(new Font(Font.SANS_SERIF, Font.BOLD, 16));
    +135    title.getAccessibleContext().setAccessibleName("iOS Skills Title");
    +136    title.setHorizontalAlignment(JLabel.LEFT);
    +137    gbc.gridx=0; gbc.gridy=0; gbc.gridwidth=2; p.add(title, gbc);
    +138
    +139        String[][] parts = new String[][]{
    +140            {"P1_1","1.1 Turn Device On/Off"},{"P1_2","1.2 Turn VoiceOver On/Off"},{"P1_3","1.3 Gestures to Click Icons"},
    +141            {"P1_4","1.4 Home Screen Icons to Open Documents"},{"P1_5","1.5 Save Documents"},{"P1_6","1.6 Online Tools/Resources"},
    +142            {"P1_7","1.7 Keyboarding"},{"P1_8","1.8 Use Different Elements"},{"P1_9","1.9 Control Center, App Switcher..."},
    +143            {"P2_1","2.1 Write, edit save"},{"P2_2","2.2 Read, Navigate Document"},{"P2_3","2.3 Use Menubar"},
    +144            {"P2_4","2.4 Highlight text, copy and paste text"},{"P2_5","2.5 Copy and paste images"},{"P2_6","2.6 Proofread and edit"},
    +145            {"P3_1","3.1 Describe Spreadsheet"},{"P3_2","3.2 Explain terms and concepts"},{"P3_3","3.3 Enter/Edit data"},
    +146            {"P3_4","3.4 Navigate Spreadsheet"},{"P3_5","3.5 Create Graphs"},{"P4_1","4.1 Create Presentation"},
    +147            {"P4_2","4.2 Edit Slides"},{"P4_3","4.3 Add Images"},{"P4_4","4.4 Present Slides"},{"P4_5","4.5 Share Presentation"},
    +148            {"P5_1","5.1 Acceptable Use Policy"},{"P5_2","5.2 Digital Citizenship"},{"P5_3","5.3 Online Safety"},
    +149            {"P5_4","5.4 Copyright"},{"P5_5","5.5 Plagiarism"},{"P5_6","5.6 Privacy"},{"P5_7","5.7 Cyberbullying"},
    +150            {"P6_1","6.1 Install Apps"},{"P6_2","6.2 Update Apps"},{"P6_3","6.3Delete Apps"},{"P6_4","6.4 Manage Storage"},
    +151            {"P6_5","6.5 Accessibility Settings"},{"P6_6","6.6 Screen Time"},{"P6_7","6.7 Parental Controls"},{"P6_8","6.8 Bluetooth"},
    +152            {"P6_9","6.9 Wi-Fi"},{"P6_10","6.10 AirDrop"},{"P6_11","6.11 Hotspot"}
    +153        };
    +154
    +155    java.awt.Font labelFont = new java.awt.Font(java.awt.Font.SANS_SERIF, java.awt.Font.PLAIN, 12);
    +156    String[] labels = java.util.Arrays.stream(parts).map(x->x[1]).toArray(String[]::new);
    +157        int maxPx = com.studentgui.uicomp.PhaseScoreField.computeMaxLabelPixelWidth(labelFont, labels);
    +158        com.studentgui.uicomp.PhaseScoreField.setGlobalLabelWidth(Math.min(360, Math.max(200, maxPx + 50)));
    +159    int row = 1;
    +160        for (String[] part : parts) {
    +161            gbc.gridx = 0; gbc.gridy = row; gbc.gridwidth = 2;
    +162            PhaseScoreField tf = new PhaseScoreField(part[1], 0);
    +163            tf.setToolTipText("Enter whole number score for " + part[1]);
    +164            tf.getAccessibleContext().setAccessibleName(part[1]);
    +165            tf.setName("ios_" + part[0]);
    +166            p.add(tf, gbc);
    +167            inputs.put(part[0], tf);
    +168            row++;
    +169        }
    +170    // Place Save and Open Latest side-by-side (Braille style)
    +171    gbc.gridx = 0; gbc.gridy = row; gbc.gridwidth = 1; gbc.anchor = GridBagConstraints.WEST;
    +172    JButton save = new JButton("Save iOS Data");
    +173    save.setPreferredSize(new java.awt.Dimension(0, 32));
    +174    save.addActionListener((ActionEvent e) -> { save(); plot(); });
    +175    save.setToolTipText("Save iOS assessment for selected student");
    +176    save.setMnemonic(KeyEvent.VK_S);
    +177    save.getAccessibleContext().setAccessibleName("Save iOS Data");
    +178    p.add(save, gbc);
    +179
    +180    gbc.gridx = 1;
    +181    JButton openLatest = new JButton("Open Latest Plot");
    +182    openLatest.setPreferredSize(new java.awt.Dimension(0, 32));
    +183    openLatest.addActionListener((ActionEvent e) -> {
    +184        java.nio.file.Path pth = com.studentgui.apphelpers.Helpers.latestPlotPath(this.studentNameParam, "iOS");
    +185        if (pth == null) {
    +186            com.studentgui.apphelpers.UiNotifier.show("No iOS plot found for student");
    +187        } else {
    +188            try {
    +189                java.awt.Desktop.getDesktop().open(pth.toFile());
    +190            } catch (java.io.IOException | UnsupportedOperationException | SecurityException ex) {
    +191                com.studentgui.apphelpers.UiNotifier.show("Unable to open plot: " + pth.getFileName().toString());
    +192            }
    +193        }
    +194    });
    +195    p.add(openLatest, gbc);
    +196
    +197    // consume remaining columns (if any) so layout stays compact and buttons are not clipped
    +198    gbc.gridx = 2; gbc.gridwidth = GridBagConstraints.REMAINDER; gbc.anchor = GridBagConstraints.WEST;
    +199    p.add(new JPanel(), gbc);
    +200    row++;
    +201
    +202        add(scroll, BorderLayout.CENTER);
    +203        add(graph, BorderLayout.SOUTH);
    +204
    +205        SwingUtilities.invokeLater(()->{
    +206            view.setPreferredSize(view.getPreferredSize());
    +207            scroll.getViewport().setViewPosition(new java.awt.Point(0,0));
    +208            revalidate();
    +209        });
    +210
    +211        SwingUtilities.invokeLater(() -> {
    +212            for (var f: inputs.values()) LOG.debug("IOS field {} labelWidth={} spinnerX={} gap={}", f.getLabel(), f.getLabelWrapWidth(), f.getSpinnerX(), f.getActualGap());
    +213        });
    +214
    +215        com.studentgui.apphelpers.Helpers.createFolderHierarchy();
    +216        initParts();
    +217    }
    +218
    +219    /**
    +220     * Ensure the iOS progress-type and part rows exist in the normalized
    +221     * database schema.
    +222     */
    +223    private void initParts() {
    +224        try {
    +225            int ptId = com.studentgui.apphelpers.Database.getOrCreateProgressType("iOS");
    +226                java.util.Set<String> keys = inputs.keySet();
    +227            String[] codes = new String[keys.size()];
    +228            int idx = 0;
    +229            for (String k : keys) {
    +230                codes[idx++] = k;
    +231            }
    +232            com.studentgui.apphelpers.Database.ensureAssessmentParts(ptId, codes);
    +233        } catch (SQLException ex) {
    +234            LOG.error("Error ensuring iOS assessment parts", ex);
    +235        }
    +236    }
    +237
    +238    /**
    +239     * Validate inputs and persist them as a new progress session for the
    +240     * selected student.
    +241     */
    +242    private void save() {
    +243        if (this.studentNameParam == null || this.studentNameParam.trim().isEmpty()) {
    +244            javax.swing.JOptionPane.showMessageDialog(this, "Please select a student before saving iOS data.", "Missing student", javax.swing.JOptionPane.WARNING_MESSAGE);
    +245            return;
    +246        }
    +247
    +248        try {
    +249            int studentId = com.studentgui.apphelpers.Database.getOrCreateStudent(this.studentNameParam);
    +250            int ptId = com.studentgui.apphelpers.Database.getOrCreateProgressType("iOS");
    +251            int sessionId = com.studentgui.apphelpers.Database.createProgressSession(studentId, ptId, this.dateParam);
    +252            java.util.Set<String> keys = inputs.keySet();
    +253            String[] codes = new String[keys.size()]; int idx = 0; for (String k: keys) codes[idx++] = k;
    +254            int[] scores = new int[codes.length];
    +255            for (int i = 0; i < codes.length; i++) {
    +256                scores[i] = inputs.get(codes[i]).getValue();
    +257            }
    +258            com.studentgui.apphelpers.Database.insertAssessmentResults(sessionId, ptId, codes, scores);
    +259            LOG.info("iOS data saved for {}", this.studentNameParam);
    +260            com.studentgui.apphelpers.UiNotifier.show("iOS data saved.");
    +261            com.studentgui.apphelpers.dto.AssessmentPayload payload = new com.studentgui.apphelpers.dto.AssessmentPayload(sessionId, codes, scores);
    +262            java.nio.file.Path jsonOut = com.studentgui.apphelpers.SessionJsonWriter.writeSessionJson(this.studentNameParam, "iOS", payload, sessionId);
    +263            if (jsonOut == null) LOG.warn("Unable to save iOS session JSON for sessionId={}", sessionId);
    +264            try {
    +265                java.nio.file.Path plotsOut = com.studentgui.apphelpers.Helpers.studentPlotsDir(this.studentNameParam);
    +266                java.nio.file.Path reportsOut = com.studentgui.apphelpers.Helpers.studentReportsDir(this.studentNameParam);
    +267                java.nio.file.Files.createDirectories(plotsOut);
    +268                java.nio.file.Files.createDirectories(reportsOut);
    +269                java.time.format.DateTimeFormatter df = java.time.format.DateTimeFormatter.ISO_DATE;
    +270                String dateStr = this.dateParam != null ? this.dateParam.format(df) : java.time.LocalDate.now().toString();
    +271                String baseName = "iOS-" + sessionId + "-" + dateStr;
    +272
    +273                com.studentgui.apphelpers.Database.ResultsWithDates rwd = com.studentgui.apphelpers.Database.fetchLatestAssessmentResultsWithDates(this.studentNameParam, "iOS", Integer.MAX_VALUE);
    +274                    java.util.Map<String, java.nio.file.Path> groups = null;
    +275                        String[] labels = new String[codes.length];
    +276                        for (int i = 0; i < codes.length; i++) {
    +277                            labels[i] = inputs.get(codes[i]).getLabel();
    +278                        }
    +279                // codes already built above as 'codes'
    +280                if (rwd != null && rwd.rows != null && !rwd.rows.isEmpty()) {
    +281                    graph.updateWithGroupedDataByDate(rwd.dates, rwd.rows, codes, labels);
    +282                    groups = graph.saveGroupedCharts(plotsOut, baseName, 1000, 240);
    +283                    java.time.LocalDate headerDate = rwd.dates.get(rwd.dates.size() - 1);
    +284                    dateStr = headerDate.format(df);
    +285                } else {
    +286                    java.util.List<java.util.List<Integer>> rowsList = new java.util.ArrayList<>();
    +287                        java.util.List<Integer> latest = new java.util.ArrayList<>();
    +288                        for (String c : codes) {
    +289                            latest.add(inputs.get(c).getValue());
    +290                        }
    +291                        rowsList.add(latest);
    +292                    graph.updateWithGroupedData(rowsList, codes);
    +293                    groups = graph.saveGroupedCharts(plotsOut, baseName, 1000, 240);
    +294                }
    +295
    +296                if (groups == null) {
    +297                    groups = new java.util.LinkedHashMap<>();
    +298                }
    +299                StringBuilder md = new StringBuilder();
    +300                md.append("# ").append(this.studentNameParam == null ? "Unknown Student" : this.studentNameParam).append(" - ").append(dateStr).append("\n\n");
    +301                for (java.util.Map.Entry<String, java.nio.file.Path> e : groups.entrySet()) {
    +302                    md.append("## ").append(e.getKey()).append("\n\n");
    +303                    md.append("![](../plots/").append(e.getValue().getFileName().toString()).append(")\n\n");
    +304                }
    +305                java.nio.file.Path mdFile = reportsOut.resolve(baseName + ".md");
    +306                java.nio.file.Files.writeString(mdFile, md.toString(), java.nio.charset.StandardCharsets.UTF_8);
    +307
    +308                try {
    +309                    String[] palette = JLineGraph.PALETTE_HEX;
    +310                    java.util.LinkedHashMap<String, java.util.List<Integer>> groupsIdx = new java.util.LinkedHashMap<>();
    +311                    for (int i = 0; i < codes.length; i++) {
    +312                        String code = codes[i];
    +313                        String grp = code != null && code.contains("_") ? code.split("_")[0] : code;
    +314                        groupsIdx.computeIfAbsent(grp, k -> new java.util.ArrayList<>()).add(i);
    +315                    }
    +316                    StringBuilder html = new StringBuilder();
    +317                    html.append("<!doctype html><html><head><meta charset=\"utf-8\"><title>");
    +318                    html.append(this.studentNameParam == null ? "Student Report" : this.studentNameParam).append(" - ").append(dateStr).append("</title>");
    +319                    html.append("<style>body{font-family:sans-serif;margin:20px;} img{max-width:100%;height:auto;border:1px solid #ccc;margin-bottom:8px;} .legend{max-height:160px;overflow:auto;border:1px solid #ddd;padding:8px;margin-bottom:24px;} .legend-item{display:flex;align-items:center;gap:8px;padding:4px 0;} .swatch{width:18px;height:12px;border:1px solid #333;display:inline-block}</style>");
    +320                    html.append("</head><body>");
    +321                    html.append("<h1>").append(this.studentNameParam == null ? "Unknown Student" : this.studentNameParam).append(" - ").append(dateStr).append("</h1>");
    +322                    for (java.util.Map.Entry<String, java.nio.file.Path> e2 : groups.entrySet()) {
    +323                        String grp = e2.getKey();
    +324                        String imgName = e2.getValue().getFileName().toString();
    +325                        html.append("<h2>").append(grp).append("</h2>");
    +326                        html.append("<div class=\"plot\"><img src=\"../plots/").append(imgName).append("\" alt=\"").append(grp).append("\"></div>");
    +327                        java.util.List<Integer> idxs = groupsIdx.getOrDefault(grp, new java.util.ArrayList<>());
    +328                        html.append("<div class=\"legend\">");
    +329                        for (int s = 0; s < idxs.size(); s++) {
    +330                            int itemIdx = idxs.get(s);
    +331                            String code = codes[itemIdx];
    +332                            String human = labels[itemIdx];
    +333                            String seriesName = code + " - " + human;
    +334                            String color = palette[s % palette.length];
    +335                            html.append("<div class=\"legend-item\">");
    +336                            html.append("<span class=\"swatch\" style=\"background:");
    +337                            html.append(color);
    +338                            html.append(";\"></span>");
    +339                            html.append("<div>");
    +340                            html.append(seriesName);
    +341                            html.append("</div></div>");
    +342                        }
    +343                        html.append("</div>");
    +344                    }
    +345                    html.append("</body></html>");
    +346                    java.nio.file.Path htmlFile = reportsOut.resolve(baseName + ".html");
    +347                    java.nio.file.Files.writeString(htmlFile, html.toString(), java.nio.charset.StandardCharsets.UTF_8);
    +348                    LOG.info("Wrote iOS HTML session report {}", htmlFile);
    +349                } catch (java.io.IOException ioex) {
    +350                    LOG.warn("Unable to write iOS HTML report: {}", ioex.toString());
    +351                }
    +352            } catch (java.io.IOException ioe) {
    +353                LOG.warn("Unable to save iOS per-phase plots or markdown report: {}", ioe.toString());
    +354            }
    +355        } catch (SQLException ex) {
    +356            LOG.error("Error saving iOS data", ex);
    +357            javax.swing.JOptionPane.showMessageDialog(this, "Database error saving iOS data: " + ex.getMessage(), "Database error", javax.swing.JOptionPane.ERROR_MESSAGE);
    +358        }
    +359    }
    +360
    +361    /**
    +362     * Fetch recent iOS assessment sessions and update the shared graph view.
    +363     */
    +364    private void plot() {
    +365        LOG.info("Plot requested for {}", studentNameParam);
    +366        try {
    +367            java.util.List<java.util.List<Integer>> data = com.studentgui.apphelpers.Database.fetchLatestAssessmentResults(this.studentNameParam, "iOS", 20);
    +368            if (data != null && !data.isEmpty()) {
    +369                // Build codes array in the same order as inputs were created
    +370                String[] codes = new String[inputs.size()];
    +371                int idx = 0; for (String k: inputs.keySet()) codes[idx++] = k;
    +372                graph.updateWithGroupedData(data, codes);
    +373                // Save static PNG
    +374                if (this.studentNameParam != null && !this.studentNameParam.trim().isEmpty()) {
    +375                    try {
    +376                        java.nio.file.Path out = com.studentgui.apphelpers.Helpers.APP_HOME.resolve("StudentDataFiles").resolve(com.studentgui.apphelpers.Helpers.safeName(this.studentNameParam)).resolve("plots");
    +377                        java.nio.file.Files.createDirectories(out);
    +378                        java.time.format.DateTimeFormatter df = java.time.format.DateTimeFormatter.ISO_DATE;
    +379                        String dateStr = (this.dateParam != null ? this.dateParam.format(df) : java.time.LocalDate.now().toString());
    +380                        java.nio.file.Path file = out.resolve("iOS-" + dateStr + ".png");
    +381                        graph.saveChart(file, 800, 400);
    +382                        LOG.info("Saved iOS plot to {}", file);
    +383                        // Do not auto-open the plot here; only save it. Opening is handled
    +384                        // by submit/save handlers or the Open Latest button.
    +385                        com.studentgui.apphelpers.UiNotifier.show("iOS plot saved to " + file.toString());
    +386                    } catch (java.io.IOException ex) { LOG.warn("Unable to save iOS plot image: {}", ex.toString()); }
    +387                }
    +388            } else {
    +389                LOG.info("No iOS data to plot for {}", studentNameParam);
    +390            }
    +391        } catch (SQLException ex) {
    +392            LOG.error("Error fetching iOS data for plot", ex);
    +393        }
    +394    }
    +395}
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    + + diff --git a/target/site/apidocs/src-html/com/studentgui/apppages/InstructionalMaterials.html b/target/site/apidocs/src-html/com/studentgui/apppages/InstructionalMaterials.html new file mode 100644 index 0000000..5db7416 --- /dev/null +++ b/target/site/apidocs/src-html/com/studentgui/apppages/InstructionalMaterials.html @@ -0,0 +1,155 @@ + + + + +Source code + + + + + + +
    +
    +
    001package com.studentgui.apppages;
    +002
    +003import java.awt.BorderLayout;
    +004import java.awt.Font;
    +005import java.awt.GridBagConstraints;
    +006import java.awt.GridBagLayout;
    +007import java.awt.Insets;
    +008import java.awt.event.ActionEvent;
    +009import java.awt.event.KeyEvent;
    +010
    +011import javax.swing.JButton;
    +012import javax.swing.JLabel;
    +013import javax.swing.JPanel;
    +014import javax.swing.JScrollPane;
    +015import javax.swing.JTextArea;
    +016import javax.swing.SwingUtilities;
    +017
    +018import org.slf4j.Logger;
    +019import org.slf4j.LoggerFactory;
    +020
    +021/**
    +022 * Instructional materials and resources reference page.
    +023 *
    +024 * <p>Provides a simple placeholder panel for displaying links, documentation, or references
    +025 * to external instructional resources. This is a static informational view without data
    +026 * persistence or assessment functionality.</p>
    +027 *
    +028 * <p><b>Current Implementation:</b></p>
    +029 * <ul>
    +030 *   <li>Read-only text area with placeholder content</li>
    +031 *   <li>Refresh button (currently logs action but performs no operation)</li>
    +032 *   <li>No database persistence or session tracking</li>
    +033 *   <li>Intended for future expansion with resource links, PDF viewers, or material management UI</li>
    +034 * </ul>
    +035 *
    +036 * <p><b>Potential Future Enhancements:</b></p>
    +037 * <ul>
    +038 *   <li>Dynamic listing of student-specific materials from {@code StudentDataFiles/<student>/InstructionalMaterials/}</li>
    +039 *   <li>PDF preview integration for viewing documents inline</li>
    +040 *   <li>File upload and organization capabilities</li>
    +041 *   <li>Links to online resources (curriculum guides, training videos, vendor documentation)</li>
    +042 *   <li>Material assignment workflow (track which materials were provided to student/family)</li>
    +043 * </ul>
    +044 *
    +045 * <p>This page does not implement listener interfaces and does not interact with the database.
    +046 * It serves as a navigation target and placeholder for future resource management features.</p>
    +047 */
    +048public class InstructionalMaterials extends JPanel {
    +049    private static final Logger LOG = LoggerFactory.getLogger(InstructionalMaterials.class);
    +050
    +051    /**
    +052     * Create the Instructional Materials page.
    +053     */
    +054    public InstructionalMaterials() {
    +055        setLayout(new BorderLayout());
    +056    JPanel p = new JPanel(new GridBagLayout());
    +057    JPanel view = new JPanel(new BorderLayout());
    +058    view.add(p, BorderLayout.NORTH);
    +059    view.setBorder(javax.swing.BorderFactory.createEmptyBorder(20,20,20,20));
    +060    JScrollPane scroll = new JScrollPane(view);
    +061    scroll.getAccessibleContext().setAccessibleName("Instructional Materials scroll pane");
    +062    GridBagConstraints gbc = new GridBagConstraints(); gbc.insets=new Insets(2,2,2,2); gbc.fill=GridBagConstraints.BOTH;
    +063    JLabel title = new JLabel("Instructional Materials", JLabel.LEFT);
    +064        title.setFont(title.getFont().deriveFont(Font.BOLD,16));
    +065        title.getAccessibleContext().setAccessibleName("Instructional Materials Title");
    +066        gbc.gridx=0; gbc.gridy=0; p.add(title, gbc);
    +067
    +068    int globalLabel = com.studentgui.uicomp.PhaseScoreField.getGlobalLabelWidth();
    +069    JLabel areaLabel = new JLabel("Materials:"); areaLabel.setPreferredSize(new java.awt.Dimension(globalLabel, areaLabel.getPreferredSize().height)); gbc.gridy=1; p.add(areaLabel, gbc);
    +070    JTextArea area = new JTextArea(20,60); area.setEditable(false); area.setText("Instructional materials listing placeholder. Add docs or links here."); area.setToolTipText("Instructional materials and links"); area.getAccessibleContext().setAccessibleName("Instructional materials"); gbc.gridy=2; p.add(area, gbc);
    +071    areaLabel.setLabelFor(area);
    +072    JButton refresh = new JButton("Refresh"); refresh.addActionListener((ActionEvent e)-> LOG.info("Refresh requested")); refresh.setToolTipText("Refresh the instructional materials listing"); refresh.setMnemonic(KeyEvent.VK_R); refresh.getAccessibleContext().setAccessibleName("Refresh instructional materials"); gbc.gridy=3; p.add(refresh, gbc);
    +073
    +074        add(scroll, BorderLayout.CENTER);
    +075        SwingUtilities.invokeLater(()->{ p.setPreferredSize(p.getPreferredSize()); revalidate(); });
    +076    }
    +077}
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    + + diff --git a/target/site/apidocs/src-html/com/studentgui/apppages/JLineGraph.html b/target/site/apidocs/src-html/com/studentgui/apppages/JLineGraph.html new file mode 100644 index 0000000..b37b004 --- /dev/null +++ b/target/site/apidocs/src-html/com/studentgui/apppages/JLineGraph.html @@ -0,0 +1,924 @@ + + + + +Source code + + + + + + +
    +
    +
    001package com.studentgui.apppages;
    +002
    +003import java.awt.BasicStroke;
    +004import java.awt.BorderLayout;
    +005import java.awt.Color;
    +006import java.awt.Dimension;
    +007import java.awt.Font;
    +008import java.util.List;
    +009import java.util.Random;
    +010import java.util.concurrent.ThreadLocalRandom;
    +011
    +012import javax.swing.JPanel;
    +013
    +014import org.jfree.chart.ChartFactory;
    +015import org.jfree.chart.ChartPanel;
    +016import org.jfree.chart.JFreeChart;
    +017import org.jfree.chart.annotations.XYPolygonAnnotation;
    +018import org.jfree.chart.axis.NumberAxis;
    +019import org.jfree.chart.plot.PlotOrientation;
    +020import org.jfree.chart.plot.XYPlot;
    +021import org.jfree.chart.renderer.xy.XYLineAndShapeRenderer;
    +022import org.jfree.data.xy.XYSeries;
    +023import org.jfree.data.xy.XYSeriesCollection;
    +024
    +025/**
    +026 * Reusable JFreeChart-based line chart component for visualizing student assessment progress.
    +027 *
    +028 * <p>This component is shared across all assessment pages (Braille, Abacus, iOS, ScreenReader, etc.)
    +029 * to display time-series data showing skill progression over multiple sessions. It supports three
    +030 * primary visualization modes:</p>
    +031 *
    +032 * <ul>
    +033 *   <li><b>Single-chart mode:</b> {@link #updateWithData(java.util.List)} - Plots all skills on one
    +034 *       chart with historical sessions in gray and the latest session highlighted in black</li>
    +035 *   <li><b>Grouped mode (session indices):</b> {@link #updateWithGroupedData(java.util.List, String[])} -
    +036 *       Creates multiple stacked charts, one per phase group (determined by part code prefix like "P1", "P2")</li>
    +037 *   <li><b>Grouped mode (chronological dates):</b> {@link #updateWithGroupedDataByDate(java.util.List, java.util.List, String[], String[])} -
    +038 *       Plots grouped data with actual dates on the X-axis for true time-series visualization</li>
    +039 * </ul>
    +040 *
    +041 * <p><b>Visual Design and Rendering:</b></p>
    +042 * <ul>
    +043 *   <li><b>Background bands:</b> Colored horizontal bands indicate score ranges to aid interpretation:
    +044 *     <ul>
    +045 *       <li><span style="color:red;">Red band</span>: -0.25 to 0.5 (minimal/no proficiency)</li>
    +046 *       <li><span style="color:orange;">Orange bands</span>: 0.5\u20131.5, 1.5\u20132.5 (emerging skills)</li>
    +047 *       <li><span style="color:yellow;">Yellow band</span>: 2.5\u20133.5 (developing proficiency)</li>
    +048 *       <li><span style="color:green;">Green band</span>: 3.5\u20134.5 (mastery/proficient)</li>
    +049 *     </ul>
    +050 *   </li>
    +051 *   <li><b>Rendering jitter:</b> A configurable visual jitter of ±{@value #JITTER_AMPLITUDE} is applied
    +052 *       to plotted points via {@link #addJitter(double)} to reveal overlapping data points. This is a
    +053 *       display-only transformation and does not modify persisted values. Jitter can be:
    +054 *     <ul>
    +055 *       <li>Enabled/disabled via {@link #setJitterEnabled(boolean)}</li>
    +056 *       <li>Made deterministic (for testing) via {@link #setJitterDeterministic(boolean)} and {@link #setJitterSeed(Long)}</li>
    +057 *       <li>Configured via {@link com.studentgui.apphelpers.Settings} keys: "jitter.enabled", "jitter.deterministic", "jitter.seed"</li>
    +058 *     </ul>
    +059 *   </li>
    +060 *   <li><b>Color palette:</b> Consistent color-blind friendly palette used for series rendering:
    +061 *     <ul>
    +062 *       <li>{@link #PALETTE_HEX}: Hex color strings for HTML legend generation (8 colors)</li>
    +063 *       <li>{@link #PALETTE}: AWT Color objects for JFreeChart rendering (8 colors matching PALETTE_HEX)</li>
    +064 *     </ul>
    +065 *   </li>
    +066 * </ul>
    +067 *
    +068 * <p><b>Typical Workflow for Assessment Pages:</b></p>
    +069 * <ol>
    +070 *   <li>Page fetches recent sessions from database via {@link com.studentgui.apphelpers.Database#fetchLatestAssessmentResultsWithDates}</li>
    +071 *   <li>Page calls {@link #updateWithGroupedDataByDate(java.util.List, java.util.List, String[], String[])} to populate chart</li>
    +072 *   <li>On submit, page calls {@link #saveGroupedCharts(java.nio.file.Path, String, int, int)} to export PNG images</li>
    +073 *   <li>Page generates Markdown/HTML reports linking to the exported plots</li>
    +074 * </ol>
    +075 *
    +076 * <p><b>Export and Persistence:</b></p>
    +077 * <ul>
    +078 *   <li>{@link #saveGroupedCharts(java.nio.file.Path, String, int, int)} - Exports each phase group as a separate PNG file</li>
    +079 *   <li>{@link #saveChart(java.nio.file.Path, int, int)} - Exports the single main chart (when not in grouped mode)</li>
    +080 *   <li>Returns Map&lt;groupName, filePath&gt; for use in report generation</li>
    +081 * </ul>
    +082 *
    +083 * <p><b>Accessibility:</b></p>
    +084 * <ul>
    +085 *   <li>ChartPanel accessible name set to "Skill progression chart"</li>
    +086 *   <li>Tooltips enabled showing coordinate values on hover</li>
    +087 *   <li>Keyboard navigation supported through JFreeChart's default ChartPanel behavior</li>
    +088 * </ul>
    +089 *
    +090 * <p><b>Settings Integration:</b> Implements {@link com.studentgui.app.SettingsChangeListener} to respond
    +091 * to jitter configuration changes at runtime without requiring application restart.</p>
    +092 *
    +093 * @see com.studentgui.apphelpers.Database#fetchLatestAssessmentResultsWithDates
    +094 * @see com.studentgui.app.SettingsChangeListener
    +095 * @see org.jfree.chart.JFreeChart
    +096 * @see org.jfree.chart.ChartPanel
    +097 */
    +098public class JLineGraph extends JPanel implements com.studentgui.app.SettingsChangeListener {
    +099    private static final long serialVersionUID = 1L;
    +100    private static final org.slf4j.Logger LOG = org.slf4j.LoggerFactory.getLogger(JLineGraph.class);
    +101    /** The dataset containing XY series for historical and latest sessions. */
    +102    private final XYSeriesCollection lineDataset;
    +103    /** The JFreeChart instance used to render the plot. */
    +104    private final JFreeChart chart;
    +105    /** Panel that embeds the chart and provides UI features. */
    +106    private final ChartPanel chartPanel;
    +107    /** When rendering grouped charts we place multiple ChartPanels in this container. */
    +108    private javax.swing.JPanel multiChartContainer;
    +109    /** Domain axis used to customise X-axis labels and range. */
    +110    private final NumberAxis xAxis;
    +111    /** Expected number of skill columns per session. */
    +112    private static final int NUMBER_OF_SKILLS = 28; // Adjust as needed
    +113    /** Jitter amplitude (plus/minus) applied to plotted data points. */
    +114    private static final double JITTER_AMPLITUDE = 0.10d;
    +115
    +116    /** Whether rendering jitter is currently enabled. Default: true. */
    +117    private boolean jitterEnabled = true;
    +118    /** When true, use a deterministic java.util.Random seeded RNG instead of ThreadLocalRandom. */
    +119    private boolean jitterDeterministic = false;
    +120    /** Optional seed used when deterministic jitter is enabled. */
    +121    private Long jitterSeed = null;
    +122    /** Cached Random instance when deterministic mode is enabled. */
    +123    private Random deterministicRandom = null;
    +124
    +125    /**
    +126     * Add a small random jitter within +/- JITTER_AMPLITUDE to the provided value.
    +127     * When jitter is disabled this returns the original value unchanged.
    +128     */
    +129    private double addJitter(final double v) {
    +130        if (!jitterEnabled) {
    +131            return v;
    +132        }
    +133        try {
    +134            if (jitterDeterministic) {
    +135                if (deterministicRandom == null) {
    +136                    long seed = jitterSeed == null ? 0L : jitterSeed.longValue();
    +137                    deterministicRandom = new Random(seed);
    +138                }
    +139                double r = deterministicRandom.nextDouble() * 2.0 - 1.0; // -1..1
    +140                return v + (r * JITTER_AMPLITUDE);
    +141            } else {
    +142                return v + ThreadLocalRandom.current().nextDouble(-JITTER_AMPLITUDE, JITTER_AMPLITUDE);
    +143            }
    +144        } catch (Throwable t) {
    +145            // In the unlikely event RNG is unavailable, fall back to no jitter
    +146            return v;
    +147        }
    +148    }
    +149    /** Public color palette (hex) for HTML legends and consistency across pages. */
    +150    public static final String[] PALETTE_HEX = new String[] {
    +151        "#1b9e77","#d95f02","#7570b3","#e7298a","#66a61e","#e6ab02","#a6761d","#666666"
    +152    };
    +153    /** Public color palette as AWT Color objects for chart rendering. */
    +154    public static final java.awt.Color[] PALETTE = new java.awt.Color[] {
    +155        new java.awt.Color(0x1b9e77),
    +156        new java.awt.Color(0xd95f02),
    +157        new java.awt.Color(0x7570b3),
    +158        new java.awt.Color(0xe7298a),
    +159        new java.awt.Color(0x66a61e),
    +160        new java.awt.Color(0xe6ab02),
    +161        new java.awt.Color(0xa6761d),
    +162        new java.awt.Color(0x666666)
    +163    };
    +164
    +165    /**
    +166     * Create a new JLineGraph with default styling and an empty dataset.
    +167     */
    +168    public JLineGraph() {
    +169        setLayout(new BorderLayout());
    +170        lineDataset = new XYSeriesCollection();
    +171
    +172        // Create a chart
    +173        chart = ChartFactory.createXYLineChart(
    +174                "Skill Progression",
    +175                "Skills",
    +176                "Value",
    +177                lineDataset,
    +178                PlotOrientation.VERTICAL,
    +179                true,
    +180                true,
    +181                false
    +182        );
    +183
    +184        // Customize the plot
    +185        XYPlot plot = chart.getXYPlot();
    +186        plot.setBackgroundPaint(Color.WHITE);
    +187        plot.setDomainGridlinePaint(Color.GRAY);
    +188        plot.setRangeGridlinePaint(Color.GRAY);
    +189
    +190        // Set axis ranges
    +191        xAxis = (NumberAxis) plot.getDomainAxis();
    +192        xAxis.setRange(0, NUMBER_OF_SKILLS + 1);
    +193        NumberAxis yAxis = (NumberAxis) plot.getRangeAxis();
    +194        yAxis.setRange(-0.25, 4.25);
    +195
    +196        // Create background bands
    +197        addBackgroundBands(plot);
    +198
    +199        chartPanel = new ChartPanel(chart);
    +200        chartPanel.setPreferredSize(new Dimension(800, 600));
    +201        chartPanel.getAccessibleContext().setAccessibleName("Skill progression chart");
    +202        chartPanel.setToolTipText("Skill progression chart showing historical and latest values");
    +203        add(chartPanel, BorderLayout.CENTER);
    +204        multiChartContainer = null;
    +205
    +206        // Set custom X-axis labels
    +207        updateXAxisLabels();
    +208        // Apply any persisted settings at creation time
    +209        try {
    +210            settingsChanged();
    +211        } catch (Throwable t) {
    +212            // ignore any issues reading settings at startup
    +213        }
    +214    }
    +215
    +216    @Override
    +217    public void settingsChanged() {
    +218        try {
    +219            String je = com.studentgui.apphelpers.Settings.get("jitter.enabled", String.valueOf(this.jitterEnabled));
    +220            setJitterEnabled("true".equalsIgnoreCase(je));
    +221            String jd = com.studentgui.apphelpers.Settings.get("jitter.deterministic", String.valueOf(this.jitterDeterministic));
    +222            setJitterDeterministic("true".equalsIgnoreCase(jd));
    +223            String s = com.studentgui.apphelpers.Settings.get("jitter.seed", this.jitterSeed == null ? "" : String.valueOf(this.jitterSeed));
    +224            if (s == null || s.trim().isEmpty()) {
    +225                setJitterSeed(null);
    +226            } else {
    +227                try {
    +228                    long v = Long.parseLong(s.trim());
    +229                    setJitterSeed(Long.valueOf(v));
    +230                } catch (NumberFormatException nfe) {
    +231                    setJitterSeed(null);
    +232                }
    +233            }
    +234            // reset cached RNG so seed/cfg takes effect
    +235            this.deterministicRandom = null;
    +236            if (chart != null) {
    +237                chart.fireChartChanged();
    +238            }
    +239            if (chartPanel != null) {
    +240                chartPanel.repaint();
    +241            }
    +242        } catch (Throwable t) {
    +243            LOG.debug("Failed applying settings: {}", t.toString());
    +244        }
    +245    }
    +246
    +247    /**
    +248     * Add lightly-colored horizontal bands to the plot to indicate score
    +249     * ranges.
    +250     */
    +251    private void addBackgroundBands(final XYPlot plot) {
    +252        // Use the generic band painter to draw the requested bands across the
    +253        // full X domain of the main chart.
    +254        double left = 0.0;
    +255        double right = NUMBER_OF_SKILLS + 1;
    +256        addHorizontalBands(plot, left, right);
    +257    }
    +258
    +259    /**
    +260     * Add horizontal background bands to the provided plot between left and right
    +261     * X coordinates. Bands follow the requested ranges:
    +262     * red = -0.25..0.5, orange = 0.5..1.5, orange = 1.5..2.5, yellow = 2.5..3.5,
    +263     * green = 3.5..4.5
    +264     */
    +265    private void addHorizontalBands(final XYPlot plot, final double left, final double right) {
    +266        try {
    +267            java.awt.Color red = new java.awt.Color(255, 0, 0, 40);
    +268            java.awt.Color orange = new java.awt.Color(255, 165, 0, 40);
    +269            java.awt.Color orange2 = new java.awt.Color(255, 140, 0, 40);
    +270            java.awt.Color yellow = new java.awt.Color(255, 255, 0, 40);
    +271            java.awt.Color green = new java.awt.Color(0, 255, 0, 40);
    +272
    +273            double[][] bands = new double[][]{
    +274                { -0.25, 0.5 },
    +275                {  0.5,  1.5 },
    +276                {  1.5,  2.5 },
    +277                {  2.5,  3.5 },
    +278                {  3.5,  4.5 }
    +279            };
    +280            java.awt.Color[] colors = new java.awt.Color[] { red, orange, orange2, yellow, green };
    +281
    +282            for (int i = 0; i < bands.length; i++) {
    +283                double low = bands[i][0];
    +284                double high = bands[i][1];
    +285                double[] coords = new double[] { left, low, right, low, right, high, left, high };
    +286                plot.addAnnotation(new XYPolygonAnnotation(coords, null, null, colors[i]));
    +287            }
    +288        } catch (Throwable t) {
    +289            LOG.debug("Unable to add horizontal bands: {}", t.toString());
    +290        }
    +291    }
    +292
    +293    /**
    +294     * Enable or disable rendering jitter at runtime.
    +295     * @param enabled true to enable jitter, false to draw raw values
    +296     */
    +297    public void setJitterEnabled(final boolean enabled) {
    +298        this.jitterEnabled = enabled;
    +299    }
    +300
    +301    /**
    +302     * Query whether rendering jitter is currently enabled.
    +303     *
    +304     * @return true when jitter is enabled, false otherwise
    +305     */
    +306    public boolean isJitterEnabled() {
    +307        return this.jitterEnabled;
    +308    }
    +309
    +310    /**
    +311     * Enable/disable deterministic (seeded) jitter.
    +312     * When enabled, jitter will be generated from a java.util.Random seeded
    +313     * with {@link #jitterSeed} (or 0 when seed is null).
    +314     *
    +315     * @param deterministic true to use a seeded RNG, false to use non-deterministic RNG
    +316     */
    +317    public void setJitterDeterministic(final boolean deterministic) {
    +318        this.jitterDeterministic = deterministic;
    +319        this.deterministicRandom = null; // reset instance so seed takes effect
    +320    }
    +321
    +322    /**
    +323     * Query whether deterministic jitter is enabled.
    +324     *
    +325     * @return true when deterministic (seeded) jitter is enabled
    +326     */
    +327    public boolean isJitterDeterministic() {
    +328        return this.jitterDeterministic;
    +329    }
    +330
    +331    /**
    +332     * Set the seed used when deterministic jitter is enabled. Pass null to
    +333     * clear the seed (will use 0 when a deterministic RNG is created).
    +334     *
    +335     * @param seed seed value or null to clear
    +336     */
    +337    public void setJitterSeed(final Long seed) {
    +338        this.jitterSeed = seed;
    +339        this.deterministicRandom = null;
    +340    }
    +341
    +342    /**
    +343     * Return the currently configured jitter seed or null when unset.
    +344     *
    +345     * @return configured seed value or null when not set
    +346     */
    +347    public Long getJitterSeed() {
    +348        return this.jitterSeed;
    +349    }
    +350
    +351    /**
    +352     * Replace the current dataset with the provided list of skill value
    +353     * series. Each inner list represents a single session and must contain
    +354     * NUMBER_OF_SKILLS entries.
    +355     *
    +356     * @param allSkillValues list of sessions where each session is a list of
    +357     *                       integer skill values (older sessions first)
    +358     */
    +359    public void updateWithData(final List<List<Integer>> allSkillValues) {
    +360        LOG.debug("updateWithData called with {} rows", allSkillValues == null ? 0 : allSkillValues.size());
    +361        if (allSkillValues == null || allSkillValues.isEmpty()) {
    +362            return;
    +363        }
    +364        // Fallback to existing single-chart behavior
    +365        lineDataset.removeAllSeries();
    +366        XYLineAndShapeRenderer renderer = new XYLineAndShapeRenderer();
    +367
    +368        // Add historical data series (each prior session as a separate series)
    +369        for (int s = 0; s < allSkillValues.size() - 1; s++) {
    +370            XYSeries hs = new XYSeries("S" + s);
    +371            List<Integer> skillValues = allSkillValues.get(s);
    +372            if (skillValues == null) {
    +373                continue;
    +374            }
    +375            for (int j = 0; j < skillValues.size(); j++) {
    +376                Integer v = skillValues.get(j);
    +377                double y = (double) (v == null ? 0 : v);
    +378                hs.add(j + 1, addJitter(y));
    +379            }
    +380            lineDataset.addSeries(hs);
    +381            renderer.setSeriesPaint(s, Color.GRAY);
    +382            renderer.setSeriesStroke(s, new BasicStroke(2.0f));
    +383            renderer.setSeriesShapesVisible(s, false);
    +384        }
    +385
    +386        // Latest session
    +387        XYSeries latestSeries = new XYSeries("Latest");
    +388        List<Integer> latestSkillValues = allSkillValues.get(allSkillValues.size() - 1);
    +389        if (latestSkillValues != null) {
    +390            for (int i = 0; i < latestSkillValues.size(); i++) {
    +391                Integer v = latestSkillValues.get(i);
    +392                double y = (double) (v == null ? 0 : v);
    +393                latestSeries.add(i + 1, addJitter(y));
    +394            }
    +395        }
    +396        lineDataset.addSeries(latestSeries);
    +397        int latestIndex = lineDataset.getSeriesCount() - 1;
    +398        renderer.setSeriesPaint(latestIndex, Color.BLACK);
    +399        renderer.setSeriesStroke(latestIndex, new BasicStroke(3f));
    +400        renderer.setSeriesShapesVisible(latestIndex, true);
    +401        renderer.setSeriesShape(latestIndex, new java.awt.geom.Ellipse2D.Double(-6, -6, 12, 12));
    +402
    +403        chart.getXYPlot().setDataset(lineDataset);
    +404        chart.getXYPlot().setRenderer(renderer);
    +405        // Ensure Y axis range and ticks are consistent across charts
    +406        try {
    +407            NumberAxis y = (NumberAxis) chart.getXYPlot().getRangeAxis();
    +408            y.setRange(-0.25, 4.25);
    +409            y.setTickUnit(new org.jfree.chart.axis.NumberTickUnit(1));
    +410        } catch (ClassCastException ignored) {
    +411            // if range axis isn't a NumberAxis, ignore
    +412        }
    +413        chart.fireChartChanged();
    +414        chartPanel.repaint();
    +415    }
    +416
    +417    /**
    +418     * Update the component with grouped plots. Each group is determined by the
    +419     * prefix of the part code (e.g. 'P1' from 'P1_1'). For each group we render
    +420     * a separate small chart stacked vertically.
    +421     *
    +422     * @param allSkillValues list of sessions (older first) where each session is a list of integer skill values
    +423     * @param partCodes array of part codes aligned with columns in each session row
    +424     */
    +425    public void updateWithGroupedData(final List<List<Integer>> allSkillValues, final String[] partCodes) {
    +426        LOG.debug("updateWithGroupedData called with rows={} partCodes={}", allSkillValues == null ? 0 : allSkillValues.size(), partCodes == null ? 0 : partCodes.length);
    +427        // validate
    +428        if (partCodes == null || partCodes.length == 0 || allSkillValues == null || allSkillValues.isEmpty()) {
    +429            return;
    +430        }
    +431
    +432        // Build group -> indexes map preserving order of first occurrence
    +433        java.util.LinkedHashMap<String, java.util.List<Integer>> groups = new java.util.LinkedHashMap<>();
    +434        for (int i = 0; i < partCodes.length; i++) {
    +435            String code = partCodes[i];
    +436            String grp = code != null && code.contains("_") ? code.split("_")[0] : code;
    +437            groups.computeIfAbsent(grp, k -> new java.util.ArrayList<>()).add(i);
    +438        }
    +439
    +440        // Remove any single chart mode UI
    +441        removeAll();
    +442        multiChartContainer = new javax.swing.JPanel();
    +443        multiChartContainer.setLayout(new javax.swing.BoxLayout(multiChartContainer, javax.swing.BoxLayout.Y_AXIS));
    +444
    +445        // For each group create a small chart
    +446        for (var entry : groups.entrySet()) {
    +447            String grp = entry.getKey();
    +448            java.util.List<Integer> idxs = entry.getValue();
    +449            XYSeriesCollection dataset = new XYSeriesCollection();
    +450            // historical sessions: create one series per prior session
    +451            int sessions = allSkillValues.size();
    +452            for (int s = 0; s < sessions; s++) {
    +453                XYSeries series = new XYSeries(s == sessions - 1 ? "Latest" : "S" + s);
    +454                List<Integer> sessionRow = allSkillValues.get(s);
    +455                for (int k = 0; k < idxs.size(); k++) {
    +456                    int colIndex = idxs.get(k);
    +457                    int x = k + 1;
    +458                    Integer vv = (colIndex < sessionRow.size() ? sessionRow.get(colIndex) : null);
    +459                    double y = (double) (vv == null ? 0 : vv);
    +460                    series.add(x, addJitter(y));
    +461                }
    +462                dataset.addSeries(series);
    +463            }
    +464
    +465            JFreeChart subchart = ChartFactory.createXYLineChart(
    +466                    grp + " - " + (idxs.size()) + " items",
    +467                    "Skill",
    +468                    "Value",
    +469                    dataset,
    +470                    PlotOrientation.VERTICAL,
    +471                    false,
    +472                    true,
    +473                    false
    +474            );
    +475            XYPlot plot = subchart.getXYPlot();
    +476            plot.setBackgroundPaint(Color.WHITE);
    +477            XYLineAndShapeRenderer renderer = new XYLineAndShapeRenderer();
    +478            for (int s = 0; s < dataset.getSeriesCount(); s++) {
    +479                if (s == dataset.getSeriesCount() - 1) {
    +480                    renderer.setSeriesPaint(s, Color.BLACK);
    +481                    renderer.setSeriesStroke(s, new BasicStroke(2.5f));
    +482                    renderer.setSeriesShapesVisible(s, true);
    +483                    renderer.setSeriesShape(s, new java.awt.geom.Ellipse2D.Double(-4, -4, 8, 8));
    +484                } else {
    +485                    renderer.setSeriesPaint(s, Color.GRAY);
    +486                    renderer.setSeriesStroke(s, new BasicStroke(1.5f));
    +487                    renderer.setSeriesShapesVisible(s, false);
    +488                }
    +489            }
    +490            plot.setRenderer(renderer);
    +491            // Ensure Y axis range and ticks show 0..3 grid with a small lower padding for x-axis visibility
    +492            try {
    +493                NumberAxis yAxis = (NumberAxis) plot.getRangeAxis();
    +494                yAxis.setRange(-0.25, 4.25);
    +495                yAxis.setTickUnit(new org.jfree.chart.axis.NumberTickUnit(1));
    +496            } catch (ClassCastException cce) {
    +497                LOG.debug("Range axis is not a NumberAxis: {}", cce.toString());
    +498            }
    +499            NumberAxis domain = (NumberAxis) plot.getDomainAxis();
    +500            if (idxs.size() <= 1) {
    +501                // single-point chart: give a small visual range around the point
    +502                domain.setRange(0.5, 1.5);
    +503            } else {
    +504                domain.setRange(1, idxs.size());
    +505            }
    +506
    +507            ChartPanel cp = new ChartPanel(subchart);
    +508            // Store the group id on the panel so callers can name files per-group
    +509            cp.setName(grp);
    +510            cp.setPreferredSize(new Dimension(800, Math.max(100, 40 * idxs.size())));
    +511            cp.setMaximumSize(new Dimension(Integer.MAX_VALUE, cp.getPreferredSize().height));
    +512            multiChartContainer.add(cp);
    +513        }
    +514
    +515        add(new javax.swing.JScrollPane(multiChartContainer), BorderLayout.CENTER);
    +516        revalidate();
    +517        repaint();
    +518    }
    +519
    +520    /**
    +521     * Plot grouped data over time. Dates are used as the X axis (oldest first).
    +522     * Each skill within a group is drawn as its own line (one series per skill)
    +523     * with point markers and a color-blind friendly palette. Legend placed
    +524     * in the upper-right corner.
    +525     *
    +526     * @param dates chronological list of session dates (oldest first)
    +527     * @param rows list of session rows where each row is a list of integer scores
    +528     * @param partCodes array of part codes aligned with the columns in each row
    +529     */
    +530    public void updateWithGroupedDataByDate(final java.util.List<java.time.LocalDate> dates, final java.util.List<java.util.List<Integer>> rows, final String[] partCodes) {
    +531        // Backwards-compatible wrapper: use code strings as labels if caller didn't provide labels
    +532        String[] labels = partCodes == null ? null : partCodes.clone();
    +533        updateWithGroupedDataByDate(dates, rows, partCodes, labels);
    +534    }
    +535
    +536    /**
    +537     * Plot grouped data over time with optional human-friendly labels.
    +538     * Each provided {@code partCodes} entry maps to a column index inside
    +539     * {@code rows} and (optionally) a friendly label supplied in
    +540     * {@code partLabels}. The dates list must be ordered oldest-first and
    +541     * must be parallel to the rows list.
    +542     *
    +543     * @param dates chronological list of session dates (oldest first)
    +544     * @param rows list of session rows where each row is a list of integer scores
    +545     * @param partCodes array of part codes aligned with the columns in each row
    +546     * @param partLabels optional human friendly labels parallel to {@code partCodes}
    +547     */
    +548    public void updateWithGroupedDataByDate(final java.util.List<java.time.LocalDate> dates, final java.util.List<java.util.List<Integer>> rows, final String[] partCodes, final String[] partLabels) {
    +549        LOG.debug("updateWithGroupedDataByDate called with dates={} rows={} parts={}", dates == null ? 0 : dates.size(), rows == null ? 0 : rows.size(), partCodes == null ? 0 : partCodes.length);
    +550        if (dates == null || rows == null || partCodes == null) {
    +551            return;
    +552        }
    +553        // Build groups preserving order
    +554        java.util.LinkedHashMap<String, java.util.List<Integer>> groups = new java.util.LinkedHashMap<>();
    +555        for (int i = 0; i < partCodes.length; i++) {
    +556            String code = partCodes[i];
    +557            String grp = code != null && code.contains("_") ? code.split("_")[0] : code;
    +558            groups.computeIfAbsent(grp, k -> new java.util.ArrayList<>()).add(i);
    +559        }
    +560
    +561        // Remove any single chart mode UI
    +562        removeAll();
    +563        multiChartContainer = new javax.swing.JPanel();
    +564        multiChartContainer.setLayout(new javax.swing.BoxLayout(multiChartContainer, javax.swing.BoxLayout.Y_AXIS));
    +565
    +566        // Color-blind friendly palette (ColorBrewer Set2-like)
    +567        java.awt.Color[] palette = new java.awt.Color[] {
    +568            new java.awt.Color(0x1b9e77), // green
    +569            new java.awt.Color(0xd95f02), // orange
    +570            new java.awt.Color(0x7570b3), // purple
    +571            new java.awt.Color(0xe7298a), // pink
    +572            new java.awt.Color(0x66a61e), // olive
    +573            new java.awt.Color(0xe6ab02), // mustard
    +574            new java.awt.Color(0xa6761d), // brown
    +575            new java.awt.Color(0x666666)  // gray
    +576        };
    +577
    +578        for (var entry : groups.entrySet()) {
    +579            String grp = entry.getKey();
    +580            java.util.List<Integer> idxs = entry.getValue();
    +581            org.jfree.data.time.TimeSeriesCollection dataset = new org.jfree.data.time.TimeSeriesCollection();
    +582
    +583            // For each skill in the group, build a time series across dates
    +584            for (int k = 0; k < idxs.size(); k++) {
    +585                int colIndex = idxs.get(k);
    +586                String code = partCodes[colIndex];
    +587                String human = (partLabels != null && partLabels.length > colIndex && partLabels[colIndex] != null) ? partLabels[colIndex] : code;
    +588                String seriesName = code + " - " + human; // legend shows code plus friendly label
    +589                org.jfree.data.time.TimeSeries ts = new org.jfree.data.time.TimeSeries(seriesName);
    +590                for (int r = 0; r < rows.size(); r++) {
    +591                    java.time.LocalDate d = dates.get(r);
    +592                    java.util.List<Integer> row = rows.get(r);
    +593                    Integer vv = (colIndex < row.size()) ? row.get(colIndex) : null;
    +594                    double val = (double) (vv == null ? 0 : vv);
    +595                    org.jfree.data.time.Day day = new org.jfree.data.time.Day(java.util.Date.from(d.atStartOfDay(java.time.ZoneId.systemDefault()).toInstant()));
    +596                    ts.addOrUpdate(day, addJitter(val));
    +597                }
    +598                dataset.addSeries(ts);
    +599            }
    +600
    +601            // Title: "Phase N Progression" when grp matches P<digit(s)>
    +602            String title = (grp != null && grp.startsWith("P") && grp.length() > 1)
    +603                    ? ("Phase " + grp.substring(1) + " Progression")
    +604                    : (grp + " progression");
    +605
    +606            JFreeChart subchart = ChartFactory.createTimeSeriesChart(
    +607                    title,
    +608                    "Date",
    +609                    "Value",
    +610                    dataset,
    +611                    true,
    +612                    true,
    +613                    false
    +614            );
    +615
    +616            XYPlot plot = subchart.getXYPlot();
    +617            plot.setBackgroundPaint(java.awt.Color.WHITE);
    +618            // Add colored horizontal bands behind the data using polygon annotations
    +619            try {
    +620                // Compute domain lower/upper bounds in millis for the current dataset if available
    +621                long domainLower = Long.MIN_VALUE;
    +622                long domainUpper = Long.MAX_VALUE;
    +623                if (!dates.isEmpty()) {
    +624                    java.time.ZoneId zid = java.time.ZoneId.systemDefault();
    +625                    java.time.LocalDate first = dates.get(0);
    +626                    java.time.LocalDate last = dates.get(dates.size() - 1).plusDays(4);
    +627                    domainLower = java.util.Date.from(first.atStartOfDay(zid).toInstant()).getTime();
    +628                    domainUpper = java.util.Date.from(last.atStartOfDay(zid).toInstant()).getTime();
    +629                }
    +630                double left = domainLower == Long.MIN_VALUE ? plot.getDomainAxis().getRange().getLowerBound() : domainLower;
    +631                double right = domainUpper == Long.MAX_VALUE ? plot.getDomainAxis().getRange().getUpperBound() : domainUpper;
    +632                // Use shared helper to draw bands in the domain coordinates (millis)
    +633                addHorizontalBands(plot, left, right);
    +634            } catch (Throwable t) {
    +635                LOG.debug("Unable to add background bands as annotations: {}", t.toString());
    +636            }
    +637            org.jfree.chart.renderer.xy.XYLineAndShapeRenderer renderer = new org.jfree.chart.renderer.xy.XYLineAndShapeRenderer(true, true);
    +638            // assign colors and markers
    +639            for (int s = 0; s < dataset.getSeriesCount(); s++) {
    +640                java.awt.Color c = palette[s % palette.length];
    +641                renderer.setSeriesPaint(s, c);
    +642                renderer.setSeriesStroke(s, new java.awt.BasicStroke(2.0f));
    +643                renderer.setSeriesShapesVisible(s, true);
    +644                renderer.setSeriesShape(s, new java.awt.geom.Ellipse2D.Double(-3, -3, 6, 6));
    +645            }
    +646            plot.setRenderer(renderer);
    +647
    +648            // Ensure Y axis range and ticks show 0..3 grid with a small lower padding for x-axis visibility
    +649                try {
    +650                    NumberAxis yAxis = (NumberAxis) plot.getRangeAxis();
    +651                    yAxis.setRange(-0.25, 4.25);
    +652                    yAxis.setTickUnit(new org.jfree.chart.axis.NumberTickUnit(1));
    +653                } catch (ClassCastException cce) {
    +654                    LOG.debug("Range axis is not a NumberAxis: {}", cce.toString());
    +655                }
    +656
    +657            // Ensure Y axis range and ticks show 0..3 grid with a small lower padding for x-axis visibility
    +658            try {
    +659                org.jfree.chart.axis.DateAxis dateAxis = (org.jfree.chart.axis.DateAxis) plot.getDomainAxis();
    +660                java.text.SimpleDateFormat fmt = new java.text.SimpleDateFormat("yyyyMMdd");
    +661                dateAxis.setDateFormatOverride(fmt);
    +662                // Use the provided dates list to determine bounds (oldest first)
    +663                if (!dates.isEmpty()) {
    +664                    java.time.ZoneId zid = java.time.ZoneId.systemDefault();
    +665                    java.time.LocalDate firstDate = dates.get(0);
    +666                    java.time.LocalDate lastDate = dates.get(dates.size() - 1);
    +667                    // pad 4 days on the right to provide visual breathing room
    +668                    java.time.LocalDate paddedUpper = lastDate.plusDays(4);
    +669                    java.util.Date lower = java.util.Date.from(firstDate.atStartOfDay(zid).toInstant());
    +670                    java.util.Date upper = java.util.Date.from(paddedUpper.atStartOfDay(zid).toInstant());
    +671                    dateAxis.setRange(lower, upper);
    +672                    // one-day tick units so each datapoint maps to a single label
    +673                    dateAxis.setTickUnit(new org.jfree.chart.axis.DateTickUnit(org.jfree.chart.axis.DateTickUnitType.DAY, 1));
    +674                }
    +675            } catch (ClassCastException cce) {
    +676                LOG.debug("Domain axis is not a DateAxis: {}", cce.toString());
    +677            }
    +678
    +679            // Place legend below the plot for clarity and allow it to show codes+labels
    +680            if (subchart.getLegend() != null) {
    +681                subchart.getLegend().setPosition(org.jfree.chart.ui.RectangleEdge.BOTTOM);
    +682            }
    +683
    +684            ChartPanel cp = new ChartPanel(subchart);
    +685            cp.setName(grp);
    +686            cp.setPreferredSize(new Dimension(1000, Math.max(180, 40 * idxs.size())));
    +687            cp.setMaximumSize(new Dimension(Integer.MAX_VALUE, cp.getPreferredSize().height));
    +688            multiChartContainer.add(cp);
    +689        }
    +690
    +691        add(new javax.swing.JScrollPane(multiChartContainer), BorderLayout.CENTER);
    +692        revalidate();
    +693        repaint();
    +694    }
    +695
    +696    /**
    +697     * Save each grouped subchart as an individual PNG file. The method writes
    +698     * files named {baseName}-{group}.png into the provided directory and
    +699     * returns a map of group -> written path. Caller must ensure grouped data
    +700     * has been rendered (updateWithGroupedData called) prior to invoking this.
    +701     *
    +702     * @param dir directory to write files into
    +703     * @param baseName base filename (no extension) to prefix each file
    +704     * @param width image width in pixels
    +705     * @param heightPerGroup per-group image height in pixels
    +706     * @return ordered map of group id to written file path
    +707     * @throws java.io.IOException on I/O error
    +708     */
    +709    public java.util.Map<String, java.nio.file.Path> saveGroupedCharts(final java.nio.file.Path dir, final String baseName, final int width, final int heightPerGroup) throws java.io.IOException {
    +710        java.util.Map<String, java.nio.file.Path> out = new java.util.LinkedHashMap<>();
    +711        if (dir == null) {
    +712            throw new java.io.IOException("output dir is null");
    +713        }
    +714        java.nio.file.Files.createDirectories(dir);
    +715        if (multiChartContainer == null || multiChartContainer.getComponentCount() == 0) {
    +716            return out;
    +717        }
    +718        for (int i = 0; i < multiChartContainer.getComponentCount(); i++) {
    +719            java.awt.Component c = multiChartContainer.getComponent(i);
    +720            String grp = c.getName() != null ? c.getName() : String.valueOf(i+1);
    +721            int h = Math.max(100, heightPerGroup);
    +722            c.setSize(width, h);
    +723            c.doLayout();
    +724            java.awt.image.BufferedImage img = new java.awt.image.BufferedImage(width, h, java.awt.image.BufferedImage.TYPE_INT_ARGB);
    +725            java.awt.Graphics2D g = img.createGraphics();
    +726            g.setColor(java.awt.Color.WHITE);
    +727            g.fillRect(0, 0, width, h);
    +728            c.paint(g);
    +729            g.dispose();
    +730            java.nio.file.Path file = dir.resolve(baseName + "-" + grp + ".png");
    +731            try (java.io.OutputStream os = java.nio.file.Files.newOutputStream(file);
    +732                 javax.imageio.stream.ImageOutputStream ios = javax.imageio.ImageIO.createImageOutputStream(os)) {
    +733                boolean written = javax.imageio.ImageIO.write(img, "png", ios);
    +734                if (!written) {
    +735                    throw new java.io.IOException("No ImageWriter for png");
    +736                }
    +737            }
    +738            out.put(grp, file);
    +739        }
    +740        return out;
    +741    }
    +742
    +743    /**
    +744     * Show an empty grouped chart using the provided part codes. This will
    +745     * render one row of zeros sized to the number of parts so the UI shows
    +746     * grouped axes and placeholders even when no session data exists yet.
    +747     *
    +748     * @param partCodes array of part codes used to determine the number of columns
    +749     */
    +750    public void showEmptyGrouped(final String[] partCodes) {
    +751        if (partCodes == null) {
    +752            return;
    +753        }
    +754        List<Integer> zeros = new java.util.ArrayList<>(java.util.Collections.nCopies(partCodes.length, 0));
    +755        List<List<Integer>> rows = new java.util.ArrayList<>();
    +756        rows.add(zeros);
    +757        updateWithGroupedData(rows, partCodes);
    +758    }
    +759
    +760    /**
    +761     * Save the current chart to a PNG file. If the chart is empty this will
    +762     * still export the rendered chart panel contents.
    +763     *
    +764     * @param outputPath path to write the PNG file to
    +765     * @param width image width in pixels
    +766     * @param height image height in pixels
    +767     * @throws java.io.IOException if writing fails
    +768     */
    +769    public void saveChart(final java.nio.file.Path outputPath, final int width, final int height) throws java.io.IOException {
    +770        if (outputPath == null) {
    +771            throw new java.io.IOException("outputPath is null");
    +772        }
    +773        java.nio.file.Path parent = outputPath.getParent();
    +774        if (parent == null) {
    +775            parent = java.nio.file.Paths.get(".");
    +776        }
    +777        // Ensure parent directory exists
    +778        java.nio.file.Files.createDirectories(parent);
    +779        java.awt.image.BufferedImage img = null;
    +780        // If we are in grouped-chart mode, render the multiChartContainer component
    +781        if (multiChartContainer != null && multiChartContainer.getComponentCount() > 0) {
    +782            // Ensure layout sizes are applied
    +783            multiChartContainer.setSize(width, height);
    +784            multiChartContainer.doLayout();
    +785            img = new java.awt.image.BufferedImage(width, height, java.awt.image.BufferedImage.TYPE_INT_ARGB);
    +786            java.awt.Graphics2D g = img.createGraphics();
    +787            // paint background white to match chart look
    +788            g.setColor(java.awt.Color.WHITE);
    +789            g.fillRect(0, 0, width, height);
    +790            multiChartContainer.paint(g);
    +791            g.dispose();
    +792        } else if (chart != null) {
    +793            img = chart.createBufferedImage(width, height);
    +794        } else {
    +795            throw new java.io.IOException("No chart available to render");
    +796        }
    +797
    +798        try {
    +799            // Use an explicit OutputStream -> ImageOutputStream to avoid platform-specific ImageIO issues
    +800            try (java.io.OutputStream os = java.nio.file.Files.newOutputStream(outputPath);
    +801                 javax.imageio.stream.ImageOutputStream ios = javax.imageio.ImageIO.createImageOutputStream(os)) {
    +802                boolean written = javax.imageio.ImageIO.write(img, "png", ios);
    +803                if (!written) {
    +804                    throw new java.io.IOException("No ImageWriter available for format 'png'");
    +805                }
    +806            }
    +807        } catch (java.io.IOException ioe) {
    +808            String diag = String.format("Failed saving chart to %s (parentExists=%b, parentWritable=%b, parentIsDir=%b)",
    +809                    outputPath.toString(), java.nio.file.Files.exists(parent), java.nio.file.Files.isWritable(parent), java.nio.file.Files.isDirectory(parent));
    +810            throw new java.io.IOException(diag, ioe);
    +811        }
    +812    }
    +813
    +814    private void updateXAxisLabels() {
    +815        // Generate labels for the X-axis
    +816        String[] skillLabels = new String[NUMBER_OF_SKILLS];
    +817        int skillGroup = 1;
    +818        int skillNumber = 1;
    +819        for (int i = 0; i < NUMBER_OF_SKILLS; i++) {
    +820            skillLabels[i] = "Skill" + skillGroup + "-" + skillNumber;
    +821            skillNumber++;
    +822            if ((skillGroup == 1 && skillNumber > 6) ||
    +823                (skillGroup == 2 && skillNumber > 4) ||
    +824                (skillGroup == 3 && skillNumber > 11) ||
    +825                (skillGroup == 4 && skillNumber > 7)) {
    +826                skillGroup++;
    +827                skillNumber = 1;
    +828            }
    +829        }
    +830
    +831        // Set the custom labels on the X-axis
    +832        NumberAxis domain = (NumberAxis) chart.getXYPlot().getDomainAxis();
    +833        domain.setVerticalTickLabels(true);
    +834        domain.setTickLabelFont(new Font("SansSerif", Font.PLAIN, 8));
    +835        domain.setTickUnit(new org.jfree.chart.axis.NumberTickUnit(1) {
    +836            @Override
    +837            public String valueToString(double value) {
    +838                int index = (int) value - 1;
    +839                if (index >= 0 && index < skillLabels.length) {
    +840                    return skillLabels[index];
    +841                }
    +842                return "";
    +843            }
    +844        });
    +845    }
    +846}
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    + + diff --git a/target/site/apidocs/src-html/com/studentgui/apppages/Keyboarding.html b/target/site/apidocs/src-html/com/studentgui/apppages/Keyboarding.html new file mode 100644 index 0000000..c061f35 --- /dev/null +++ b/target/site/apidocs/src-html/com/studentgui/apppages/Keyboarding.html @@ -0,0 +1,346 @@ + + + + +Source code + + + + + + +
    +
    +
    001package com.studentgui.apppages;
    +002
    +003import java.awt.BorderLayout;
    +004import java.awt.Dimension;
    +005import java.awt.Font;
    +006import java.awt.GridBagConstraints;
    +007import java.awt.GridBagLayout;
    +008import java.awt.Insets;
    +009import java.awt.event.ActionEvent;
    +010import java.awt.event.KeyEvent;
    +011import java.sql.SQLException;
    +012import java.time.LocalDate;
    +013
    +014import javax.swing.JButton;
    +015import javax.swing.JLabel;
    +016import javax.swing.JOptionPane;
    +017import javax.swing.JPanel;
    +018import javax.swing.JScrollPane;
    +019import javax.swing.JTextField;
    +020import javax.swing.SwingUtilities;
    +021
    +022import org.slf4j.Logger;
    +023import org.slf4j.LoggerFactory;
    +024
    +025/**
    +026 * Touch-typing and keyboarding skills assessment page.
    +027 *
    +028 * <p>Unlike other assessment pages that use phase-score grids, this page captures
    +029 * structured performance metrics for keyboarding practice sessions:</p>
    +030 *
    +031 * <ul>
    +032 *   <li><b>Program:</b> Name of the typing curriculum or software (e.g., TypingClub, KeyBlaze, Braille2000)</li>
    +033 *   <li><b>Topic:</b> Specific lesson, module, or exercise completed (e.g., "Home Row Mastery", "Lesson 12")</li>
    +034 *   <li><b>Speed (WPM):</b> Words per minute achieved during the timed exercise</li>
    +035 *   <li><b>Accuracy (%):</b> Percentage of characters typed correctly</li>
    +036 * </ul>
    +037 *
    +038 * <p><b>Data Persistence:</b></p>
    +039 * <ul>
    +040 *   <li>Values persisted via {@link com.studentgui.apphelpers.Database#insertKeyboardingResult} to the {@code KeyboardingResult} table</li>
    +041 *   <li>JSON export: {@code StudentDataFiles/<student>/Sessions/Keyboarding/Keyboarding-<sessionId>-<timestamp>.json}</li>
    +042 *   <li>Metadata-only reports (no plots): Markdown and HTML files in {@code reports/} with session details</li>
    +043 * </ul>
    +044 *
    +045 * <p><b>Validation and Error Handling:</b></p>
    +046 * <ul>
    +047 *   <li>Speed and Accuracy fields must contain whole numbers (non-negative integers)</li>
    +048 *   <li>Empty speed/accuracy fields default to 0 for leniency</li>
    +049 *   <li>Invalid input triggers error dialogs and field focus for correction</li>
    +050 * </ul>
    +051 *
    +052 * <p>The shared {@link JLineGraph} component is present for UI consistency but is not populated
    +053 * with keyboarding data (keyboarding does not use assessment parts). Implements
    +054 * {@link com.studentgui.app.DateChangeListener} and {@link com.studentgui.app.StudentChangeListener}
    +055 * for title updates when global selections change.</p>
    +056 *
    +057 * @see com.studentgui.apphelpers.Database#insertKeyboardingResult
    +058 * @see com.studentgui.apphelpers.dto.KeyboardingPayload
    +059 */
    +060public class Keyboarding extends JPanel implements com.studentgui.app.DateChangeListener, com.studentgui.app.StudentChangeListener {
    +061    private static final Logger LOG = LoggerFactory.getLogger(Keyboarding.class);
    +062    /** Text field for the program or curriculum name. */
    +063    private final JTextField programField, topicField, speedField, accuracyField;
    +064
    +065    /** Shared graph component (present but not used for keyboarding plotting). */
    +066    private final JLineGraph lineGraph;
    +067
    +068    /** Selected student's display name for saves/refreshes (may be null). */
    +069    private String studentNameParam;
    +070    /** Page header label. */
    +071    private JLabel titleLabel;
    +072    /** Base title text for the Keyboarding page; date suffix appended in UI. */
    +073    private final String baseTitle = "Keyboarding Skills";
    +074
    +075    /** Session date associated with persisted keyboarding results. */
    +076    private LocalDate dateParam;
    +077
    +078    /**
    +079     * Construct the Keyboarding page for a specific student and session date.
    +080     *
    +081     * @param studentName selected student's display name (may be null)
    +082     * @param date session date used for persisted results
    +083     * @param lineGraph shared graph component (unused for keyboarding results)
    +084     */
    +085    public Keyboarding(String studentName, LocalDate date, JLineGraph lineGraph) {
    +086    this.studentNameParam = (studentName == null || studentName.trim().isEmpty()) ? com.studentgui.apphelpers.Helpers.defaultStudent() : studentName;
    +087        this.dateParam = date;
    +088        this.lineGraph = lineGraph;
    +089        setLayout(new BorderLayout());
    +090
    +091    JPanel p = new JPanel(new GridBagLayout());
    +092    JPanel view = new JPanel(new BorderLayout());
    +093    view.add(p, BorderLayout.NORTH);
    +094    view.setBorder(javax.swing.BorderFactory.createEmptyBorder(20,20,20,20));
    +095    JScrollPane scroll = new JScrollPane(view);
    +096    scroll.getAccessibleContext().setAccessibleName("Keyboarding data entry scroll pane");
    +097    p.setBorder(javax.swing.BorderFactory.createEmptyBorder(20,20,20,20));
    +098        GridBagConstraints gbc = new GridBagConstraints(); gbc.insets=new Insets(2,2,2,2); gbc.fill = GridBagConstraints.HORIZONTAL; gbc.anchor = GridBagConstraints.NORTHWEST;
    +099    this.titleLabel = new JLabel(baseTitle, JLabel.LEFT);
    +100    this.titleLabel.setFont(this.titleLabel.getFont().deriveFont(Font.BOLD,16));
    +101    this.titleLabel.getAccessibleContext().setAccessibleName("Keyboarding Skills Title");
    +102    gbc.gridx=0; gbc.gridy=0; gbc.gridwidth=2; p.add(this.titleLabel, gbc);
    +103
    +104    gbc.gridwidth=1;
    +105    // Normalize label width to the PhaseScoreField global width so inputs align
    +106    int globalLabel = com.studentgui.uicomp.PhaseScoreField.getGlobalLabelWidth();
    +107    gbc.gridy=1; gbc.gridx=0; JLabel programLabel = new JLabel("Program:"); programLabel.setPreferredSize(new Dimension(globalLabel, programLabel.getPreferredSize().height)); p.add(programLabel, gbc); gbc.gridx=1; programField = new JTextField(); programField.setPreferredSize(new Dimension(300,24)); programField.setToolTipText("Name of the program or curriculum"); programField.getAccessibleContext().setAccessibleName("Program"); p.add(programField, gbc); programLabel.setLabelFor(programField);
    +108    gbc.gridy=2; gbc.gridx=0; JLabel topicLabel = new JLabel("Topic:"); topicLabel.setPreferredSize(new Dimension(globalLabel, topicLabel.getPreferredSize().height)); p.add(topicLabel, gbc); gbc.gridx=1; topicField = new JTextField(); topicField.setPreferredSize(new Dimension(300,24)); topicField.setToolTipText("Topic or lesson name"); topicField.getAccessibleContext().setAccessibleName("Topic"); p.add(topicField, gbc); topicLabel.setLabelFor(topicField);
    +109    gbc.gridy=3; gbc.gridx=0; JLabel speedLabel = new JLabel("Speed (WPM):"); speedLabel.setPreferredSize(new Dimension(globalLabel, speedLabel.getPreferredSize().height)); p.add(speedLabel, gbc); gbc.gridx=1; speedField = new JTextField("0"); speedField.setPreferredSize(new Dimension(100,24)); speedField.setToolTipText("Words per minute"); speedField.getAccessibleContext().setAccessibleName("Speed (WPM)"); p.add(speedField, gbc); speedLabel.setLabelFor(speedField);
    +110    gbc.gridy=4; gbc.gridx=0; JLabel accuracyLabel = new JLabel("Accuracy (%):"); accuracyLabel.setPreferredSize(new Dimension(globalLabel, accuracyLabel.getPreferredSize().height)); p.add(accuracyLabel, gbc); gbc.gridx=1; accuracyField = new JTextField("0"); accuracyField.setPreferredSize(new Dimension(100,24)); accuracyField.setToolTipText("Accuracy percentage"); accuracyField.getAccessibleContext().setAccessibleName("Accuracy (%)"); p.add(accuracyField, gbc); accuracyLabel.setLabelFor(accuracyField);
    +111
    +112    gbc.gridy=5; gbc.gridx=0; gbc.gridwidth=GridBagConstraints.REMAINDER;
    +113    JButton submit = new JButton("Submit Data");
    +114    submit.setPreferredSize(new java.awt.Dimension(0, 32));
    +115    submit.addActionListener((ActionEvent e)-> { submitData(); refreshGraph(); });
    +116    submit.setToolTipText("Save keyboarding result for selected student");
    +117    submit.setMnemonic(KeyEvent.VK_S);
    +118    submit.getAccessibleContext().setAccessibleName("Submit Keyboarding Data");
    +119    p.add(submit, gbc);
    +120    gbc.gridwidth = 1;
    +121    // Removed separate Refresh Graph button; Submit Data now triggers refreshGraph
    +122
    +123    add(scroll, BorderLayout.CENTER);
    +124    add(this.lineGraph, BorderLayout.SOUTH);
    +125
    +126    SwingUtilities.invokeLater(()->{ p.setPreferredSize(p.getPreferredSize()); updateTitleDate(); revalidate(); });
    +127
    +128        com.studentgui.apphelpers.Helpers.createFolderHierarchy();
    +129        initDatabase();
    +130        refreshGraph();
    +131    }
    +132
    +133    /**
    +134     * Ensure the keyboarding progress type exists in the canonical schema.
    +135     */
    +136    private void initDatabase() {
    +137        try {
    +138            com.studentgui.apphelpers.Database.getOrCreateProgressType("Keyboarding");
    +139        } catch (SQLException ex) {
    +140            LOG.error("Error ensuring Keyboarding progress type", ex);
    +141        }
    +142    }
    +143
    +144    /**
    +145     * Validate keyboarding inputs (speed and accuracy as integers) and
    +146     * persist a keyboarding result record for the selected student.
    +147     */
    +148    private void submitData() {
    +149        if (this.studentNameParam == null || this.studentNameParam.trim().isEmpty()) {
    +150            JOptionPane.showMessageDialog(this, "Please select a student before saving keyboarding data.", "Missing student", JOptionPane.WARNING_MESSAGE);
    +151            return;
    +152        }
    +153
    +154        try {
    +155            int studentId = com.studentgui.apphelpers.Database.getOrCreateStudent(this.studentNameParam);
    +156            int ptId = com.studentgui.apphelpers.Database.getOrCreateProgressType("Keyboarding");
    +157            int sessionId = com.studentgui.apphelpers.Database.createProgressSession(studentId, ptId, this.dateParam);
    +158
    +159            String program = programField.getText().trim();
    +160            String topic = topicField.getText().trim();
    +161            int speed;
    +162            int accuracy;
    +163            try {
    +164                String sp = speedField.getText().trim(); speed = sp.isEmpty() ? 0 : Integer.parseInt(sp);
    +165            } catch (NumberFormatException nfe) {
    +166                JOptionPane.showMessageDialog(this, "Please enter a whole number for Speed (WPM)", "Invalid input", JOptionPane.ERROR_MESSAGE);
    +167                speedField.requestFocusInWindow();
    +168                return;
    +169            }
    +170            try {
    +171                String ac = accuracyField.getText().trim(); accuracy = ac.isEmpty() ? 0 : Integer.parseInt(ac);
    +172            } catch (NumberFormatException nfe) {
    +173                JOptionPane.showMessageDialog(this, "Please enter a whole number for Accuracy (%)", "Invalid input", JOptionPane.ERROR_MESSAGE);
    +174                accuracyField.requestFocusInWindow();
    +175                return;
    +176            }
    +177
    +178            com.studentgui.apphelpers.Database.insertKeyboardingResult(sessionId, program, topic, speed, accuracy);
    +179            LOG.info("Keyboarding data saved for {}", this.studentNameParam);
    +180            com.studentgui.apphelpers.UiNotifier.show("Keyboarding data saved.");
    +181            com.studentgui.apphelpers.dto.KeyboardingPayload payload = new com.studentgui.apphelpers.dto.KeyboardingPayload(sessionId, program, topic, speed, accuracy);
    +182            java.nio.file.Path jsonOut = com.studentgui.apphelpers.SessionJsonWriter.writeSessionJson(this.studentNameParam, "Keyboarding", payload, sessionId);
    +183            if (jsonOut == null) {
    +184                LOG.warn("Unable to save Keyboarding session JSON for sessionId={}", sessionId);
    +185            }
    +186            try {
    +187                java.nio.file.Path plotsOut = com.studentgui.apphelpers.Helpers.studentPlotsDir(this.studentNameParam);
    +188                java.nio.file.Path reportsOut = com.studentgui.apphelpers.Helpers.studentReportsDir(this.studentNameParam);
    +189                java.nio.file.Files.createDirectories(plotsOut);
    +190                java.nio.file.Files.createDirectories(reportsOut);
    +191                java.time.format.DateTimeFormatter df = java.time.format.DateTimeFormatter.ISO_DATE;
    +192                String dateStr = this.dateParam != null ? this.dateParam.format(df) : java.time.LocalDate.now().toString();
    +193                String baseName = "Keyboarding-" + sessionId + "-" + dateStr;
    +194
    +195                // Keyboarding doesn't have grouped codes; produce a small HTML/MD with metadata
    +196                StringBuilder md = new StringBuilder();
    +197                md.append("# ").append(this.studentNameParam == null ? "Unknown Student" : this.studentNameParam).append(" - ").append(dateStr).append("\n\n");
    +198                md.append("**Program:** ").append(program == null || program.isEmpty() ? "(none)" : program).append("  \n\n");
    +199                md.append("**Topic:** ").append(topic == null || topic.isEmpty() ? "(none)" : topic).append("  \n\n");
    +200                md.append("**Speed (WPM):** ").append(String.valueOf(speed)).append("  \n\n");
    +201                md.append("**Accuracy (%):** ").append(String.valueOf(accuracy)).append("  \n\n");
    +202                java.nio.file.Path mdFile = reportsOut.resolve(baseName + ".md");
    +203                java.nio.file.Files.writeString(mdFile, md.toString(), java.nio.charset.StandardCharsets.UTF_8);
    +204
    +205                try {
    +206                    StringBuilder html = new StringBuilder();
    +207                    html.append("<!doctype html><html><head><meta charset=\"utf-8\"><title>");
    +208                    html.append(this.studentNameParam == null ? "Student Report" : this.studentNameParam).append(" - ").append(dateStr).append("</title>");
    +209                    html.append("<style>body{font-family:sans-serif;margin:20px;} .meta{margin-bottom:12px;} .swatch{width:18px;height:12px;border:1px solid #333;display:inline-block;vertical-align:middle;margin-right:8px;}</style>");
    +210                    html.append("</head><body>");
    +211                    html.append("<h1>").append(this.studentNameParam == null ? "Unknown Student" : this.studentNameParam).append(" - ").append(dateStr).append("</h1>");
    +212                    html.append("<div class=\"meta\">\n");
    +213                    html.append("<p><strong>Program:</strong> ").append(program == null || program.isEmpty() ? "(none)" : program).append("</p>");
    +214                    html.append("<p><strong>Topic:</strong> ").append(topic == null || topic.isEmpty() ? "(none)" : topic).append("</p>");
    +215                    html.append("<p><strong>Speed (WPM):</strong> ").append(String.valueOf(speed)).append("</p>");
    +216                    html.append("<p><strong>Accuracy (%):</strong> ").append(String.valueOf(accuracy)).append("</p>");
    +217                    html.append("</div>");
    +218                    html.append("</body></html>");
    +219                    java.nio.file.Path htmlFile = reportsOut.resolve(baseName + ".html");
    +220                    java.nio.file.Files.writeString(htmlFile, html.toString(), java.nio.charset.StandardCharsets.UTF_8);
    +221                    LOG.info("Wrote Keyboarding session report {}", htmlFile);
    +222                } catch (java.io.IOException ioex) {
    +223                    LOG.warn("Unable to write Keyboarding HTML report: {}", ioex.toString());
    +224                }
    +225            } catch (java.io.IOException ioe) {
    +226                LOG.warn("Unable to save Keyboarding report: {}", ioe.toString());
    +227            }
    +228        } catch (SQLException ex) {
    +229            LOG.error("DB error saving keyboarding data", ex);
    +230            JOptionPane.showMessageDialog(this, "Database error saving keyboarding data: " + ex.getMessage(), "Database error", JOptionPane.ERROR_MESSAGE);
    +231        }
    +232    }
    +233
    +234    /**
    +235     * Refresh the keyboarding visualization. Currently keyboarding results are
    +236     * stored in a separate table and this method logs the request.
    +237     */
    +238    private void refreshGraph() {
    +239        LOG.info("Keyboarding refresh requested for {}", studentNameParam);
    +240    }
    +241
    +242    @Override
    +243    public void dateChanged(LocalDate newDate) {
    +244        this.dateParam = newDate;
    +245        SwingUtilities.invokeLater(() -> {
    +246            refreshGraph();
    +247            updateTitleDate();
    +248        });
    +249    }
    +250
    +251    @Override
    +252    public void studentChanged(String newStudent) {
    +253        this.studentNameParam = newStudent;
    +254        SwingUtilities.invokeLater(() -> {
    +255            refreshGraph();
    +256            updateTitleDate();
    +257        });
    +258    }
    +259
    +260    private void updateTitleDate() {
    +261        try {
    +262            String dateStr = this.dateParam != null ? this.dateParam.toString() : java.time.LocalDate.now().toString();
    +263            this.titleLabel.setText(baseTitle + " - " + dateStr);
    +264        } catch (Exception ex) {
    +265            this.titleLabel.setText(baseTitle);
    +266        }
    +267    }
    +268}
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    + + diff --git a/target/site/apidocs/src-html/com/studentgui/apppages/Observations.html b/target/site/apidocs/src-html/com/studentgui/apppages/Observations.html new file mode 100644 index 0000000..0726efd --- /dev/null +++ b/target/site/apidocs/src-html/com/studentgui/apppages/Observations.html @@ -0,0 +1,223 @@ + + + + +Source code + + + + + + +
    +
    +
    001package com.studentgui.apppages;
    +002
    +003import java.awt.BorderLayout;
    +004import java.awt.Font;
    +005import java.awt.GridBagConstraints;
    +006import java.awt.GridBagLayout;
    +007import java.awt.Insets;
    +008import java.awt.event.ActionEvent;
    +009import java.awt.event.KeyEvent;
    +010import java.sql.SQLException;
    +011import java.time.LocalDate;
    +012
    +013import javax.swing.JButton;
    +014import javax.swing.JLabel;
    +015import javax.swing.JOptionPane;
    +016import javax.swing.JPanel;
    +017import javax.swing.JScrollPane;
    +018import javax.swing.JTextArea;
    +019import javax.swing.SwingUtilities;
    +020
    +021import org.slf4j.Logger;
    +022import org.slf4j.LoggerFactory;
    +023
    +024/**
    +025 * Observational notes page for documenting unstructured student behaviors and progress.
    +026 *
    +027 * <p>Similar to {@link SessionNotes} but intended for ongoing observational records rather than
    +028 * post-session reflections. Provides a multi-line text area for educators to capture qualitative
    +029 * observations throughout or across multiple sessions.</p>
    +030 *
    +031 * <p><b>Typical Use Cases:</b></p>
    +032 * <ul>
    +033 *   <li>Recording specific skill demonstrations observed in real-time (e.g., "Student independently located Braille cell for letter 'G' after 2 attempts")</li>
    +034 *   <li>Documenting spontaneous behaviors or breakthroughs (e.g., "First time student used VoiceOver gestures without prompting")</li>
    +035 *   <li>Noting patterns over time (e.g., "Third session this week where student requested breaks during Abacus work")</li>
    +036 *   <li>Functional vision assessments and CVI-related observations</li>
    +037 * </ul>
    +038 *
    +039 * <p><b>Data Persistence:</b></p>
    +040 * <ul>
    +041 *   <li>Notes saved via {@link com.studentgui.apphelpers.Database#saveSessionNotes} to {@code ProgressSession.notes} column</li>
    +042 *   <li>Associated with an Observations progress type for categorization</li>
    +043 *   <li>Dummy assessment result (code="OBS_NOTE", score=0) inserted to satisfy schema constraints</li>
    +044 *   <li>JSON export: {@code StudentDataFiles/<student>/Sessions/Observations/Observations-<sessionId>-<timestamp>.json}</li>
    +045 * </ul>
    +046 *
    +047 * <p>No plots or quantitative reports are generated. This page does not implement listener interfaces
    +048 * and operates on static student/date parameters set at construction time.</p>
    +049 *
    +050 * @see com.studentgui.apphelpers.Database#saveSessionNotes
    +051 * @see com.studentgui.apphelpers.dto.NotesPayload
    +052 * @see SessionNotes
    +053 */
    +054public class Observations extends JPanel {
    +055    private static final Logger LOG = LoggerFactory.getLogger(Observations.class);
    +056    /** Multi-line text area for entering observational notes. */
    +057    private final JTextArea notesArea;
    +058
    +059    /** Selected student's display name (may be null) for this observation session. */
    +060    private final String studentNameParam;
    +061
    +062    /** Date associated with the recorded observations. */
    +063    private final LocalDate dateParam;
    +064
    +065    /**
    +066     * Create an Observations page for the given student and date.
    +067     *
    +068     * @param studentName student display name (may be null when no student selected)
    +069     * @param date        the date this observation applies to
    +070     */
    +071    public Observations(String studentName, LocalDate date) {
    +072    this.studentNameParam = (studentName == null || studentName.trim().isEmpty()) ? com.studentgui.apphelpers.Helpers.defaultStudent() : studentName;
    +073        this.dateParam = date;
    +074        setLayout(new BorderLayout());
    +075
    +076    JPanel p = new JPanel(new GridBagLayout());
    +077    JPanel view = new JPanel(new BorderLayout());
    +078    view.add(p, BorderLayout.NORTH);
    +079    view.setBorder(javax.swing.BorderFactory.createEmptyBorder(20,20,20,20));
    +080    JScrollPane scroll = new JScrollPane(view);
    +081    scroll.getAccessibleContext().setAccessibleName("Observations data entry scroll pane");
    +082        GridBagConstraints gbc = new GridBagConstraints(); gbc.insets=new Insets(2,2,2,2); gbc.fill = GridBagConstraints.BOTH; gbc.anchor = GridBagConstraints.NORTHWEST;
    +083    JLabel title = new JLabel("Observations", JLabel.LEFT);
    +084    title.setFont(title.getFont().deriveFont(Font.BOLD,16));
    +085    title.getAccessibleContext().setAccessibleName("Observations Title");
    +086    gbc.gridx=0; gbc.gridy=0; gbc.gridwidth=1; p.add(title, gbc);
    +087
    +088    gbc.gridy=1; gbc.gridx=0; JLabel notesLabel = new JLabel("Notes:"); p.add(notesLabel, gbc);
    +089    gbc.gridy=2; gbc.gridx=0; notesArea = new JTextArea(8,40); notesArea.setLineWrap(true); notesArea.setWrapStyleWord(true); notesArea.setToolTipText("Enter observational notes for the student"); notesArea.getAccessibleContext().setAccessibleName("Observations notes"); p.add(notesArea, gbc);
    +090    notesLabel.setLabelFor(notesArea);
    +091
    +092    // Filler so the scroll content has room and the form is visible (prevents
    +093    // the shared graph in SOUTH from visually dominating the view)
    +094    gbc.gridy = 3; gbc.gridx = 0; gbc.gridwidth = GridBagConstraints.REMAINDER; gbc.weighty = 1.0;
    +095    p.add(new JPanel(), gbc);
    +096    gbc.weighty = 0.0; gbc.gridwidth = 1;
    +097
    +098    gbc.gridy = 4; JButton submit = new JButton("Save Notes");
    +099    submit.addActionListener((ActionEvent e)-> saveNotes());
    +100    submit.setMnemonic(KeyEvent.VK_S);
    +101    submit.setToolTipText("Save observational notes (Alt+S)");
    +102    submit.getAccessibleContext().setAccessibleName("Save Observations Notes");
    +103    gbc.gridx = 0; gbc.anchor = GridBagConstraints.WEST;
    +104    p.add(submit, gbc);
    +105    // consume remaining columns so layout stays consistent
    +106    gbc.gridx = 1; gbc.gridwidth = GridBagConstraints.REMAINDER; p.add(new JPanel(), gbc);
    +107
    +108    add(scroll, BorderLayout.CENTER);
    +109
    +110        SwingUtilities.invokeLater(()->{ p.setPreferredSize(p.getPreferredSize()); revalidate(); });
    +111
    +112        com.studentgui.apphelpers.Helpers.createFolderHierarchy();
    +113    }
    +114
    +115    /**
    +116     * Persist the contents of the notes area into the canonical database.
    +117     * Creates or re-uses the student, progress type and session records as needed.
    +118     */
    +119    private void saveNotes() {
    +120        if (this.studentNameParam == null || this.studentNameParam.trim().isEmpty()) {
    +121            JOptionPane.showMessageDialog(this, "Please select a student before saving observations.", "Missing student", JOptionPane.WARNING_MESSAGE);
    +122            return;
    +123        }
    +124
    +125        try {
    +126            int studentId = com.studentgui.apphelpers.Database.getOrCreateStudent(this.studentNameParam);
    +127            int ptId = com.studentgui.apphelpers.Database.getOrCreateProgressType("Observations");
    +128            int sessionId = com.studentgui.apphelpers.Database.createProgressSession(studentId, ptId, this.dateParam);
    +129            String notes = notesArea.getText();
    +130            com.studentgui.apphelpers.Database.insertAssessmentResults(sessionId, ptId, new String[]{"OBS_NOTE"}, new int[]{0});
    +131            // store the notes in the ProgressSession.notes column via helper
    +132            com.studentgui.apphelpers.Database.saveSessionNotes(sessionId, notes);
    +133            LOG.info("Saved observations for {}", studentNameParam);
    +134            com.studentgui.apphelpers.UiNotifier.show("Observations saved.");
    +135            com.studentgui.apphelpers.dto.NotesPayload payload = new com.studentgui.apphelpers.dto.NotesPayload(sessionId, notes);
    +136            java.nio.file.Path jsonOut = com.studentgui.apphelpers.SessionJsonWriter.writeSessionJson(this.studentNameParam, "Observations", payload, sessionId);
    +137            if (jsonOut == null) {
    +138                LOG.warn("Unable to save Observations session JSON for sessionId={}", sessionId);
    +139            }
    +140        } catch (SQLException ex) {
    +141            LOG.error("Error saving observations", ex);
    +142            JOptionPane.showMessageDialog(this, "Database error saving observations: " + ex.getMessage(), "Database error", JOptionPane.ERROR_MESSAGE);
    +143        }
    +144    }
    +145}
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    + + diff --git a/target/site/apidocs/src-html/com/studentgui/apppages/ScreenReader.html b/target/site/apidocs/src-html/com/studentgui/apppages/ScreenReader.html new file mode 100644 index 0000000..91a7cdf --- /dev/null +++ b/target/site/apidocs/src-html/com/studentgui/apppages/ScreenReader.html @@ -0,0 +1,507 @@ + + + + +Source code + + + + + + +
    +
    +
    001package com.studentgui.apppages;
    +002
    +003import java.awt.BorderLayout;
    +004import java.awt.Font;
    +005import java.awt.GridBagConstraints;
    +006import java.awt.GridBagLayout;
    +007import java.awt.Insets;
    +008import java.awt.event.ActionEvent;
    +009import java.awt.event.KeyEvent;
    +010import java.sql.SQLException;
    +011import java.time.LocalDate;
    +012import java.util.List;
    +013
    +014import javax.swing.JButton;
    +015import javax.swing.JLabel;
    +016import javax.swing.JOptionPane;
    +017import javax.swing.JPanel;
    +018import javax.swing.JScrollPane;
    +019import javax.swing.SwingUtilities;
    +020
    +021import org.slf4j.Logger;
    +022import org.slf4j.LoggerFactory;
    +023
    +024/**
    +025 * Screen reader proficiency assessment page for desktop/laptop environments.
    +026 *
    +027 * <p>Evaluates student competency with screen reading software (JAWS, NVDA, Narrator,
    +028 * VoiceOver macOS) across 28 standardized skills organized into 4 progressive competency phases:</p>
    +029 *
    +030 * <ul>
    +031 *   <li><b>Phase 1 (P1_1–P1_6): Fundamental Navigation and Interaction</b>
    +032 *     <ul>
    +033 *       <li>Basic keyboard navigation (Tab, arrow keys, application switching)</li>
    +034 *       <li>Reading and interpreting control labels and text content</li>
    +035 *       <li>Activating controls (buttons, links, checkboxes) via keyboard</li>
    +036 *       <li>Form entry (text fields, combo boxes, radio buttons)</li>
    +037 *       <li>Table navigation (row/column movement, header announcement)</li>
    +038 *       <li>Heading navigation (H key, heading list, semantic structure)</li>
    +039 *     </ul>
    +040 *   </li>
    +041 *   <li><b>Phase 2 (P2_1–P2_4): Web and Document Element Navigation</b>
    +042 *     <ul>
    +043 *       <li>Link navigation and link list usage</li>
    +044 *       <li>List navigation (ordered, unordered, nested lists)</li>
    +045 *       <li>Image handling (alt text, long descriptions, graphics navigation)</li>
    +046 *       <li>Annotation and metadata awareness (ARIA labels, landmarks)</li>
    +047 *     </ul>
    +048 *   </li>
    +049 *   <li><b>Phase 3 (P3_1–P3_11): Advanced Document Structures and Customization</b>
    +050 *     <ul>
    +051 *       <li>Document structure navigation (sections, articles, landmarks)</li>
    +052 *       <li>Style and formatting awareness (bold, italic, font changes)</li>
    +053 *       <li>Advanced table navigation (complex tables, merged cells, formulas)</li>
    +054 *       <li>Chart and graph interpretation with screen reader feedback</li>
    +055 *       <li>Advanced keyboard shortcuts and quick navigation commands</li>
    +056 *       <li>Scripting usage (JAWS scripts, NVDA add-ons)</li>
    +057 *       <li>Third-party application integration (Office, Adobe, IDEs)</li>
    +058 *       <li>Multimedia content handling (audio descriptions, video captions)</li>
    +059 *       <li>Braille display usage and synchronization</li>
    +060 *       <li>Braille table switching (Grade 1, Grade 2, computer braille)</li>
    +061 *       <li>Configuration and customization (speech rate, verbosity, sounds)</li>
    +062 *     </ul>
    +063 *   </li>
    +064 *   <li><b>Phase 4 (P4_1–P4_7): Efficiency, Troubleshooting, and Integration</b>
    +065 *     <ul>
    +066 *       <li>Performance optimization (adjusting verbosity, quick navigation mastery)</li>
    +067 *       <li>Error recovery strategies (finding lost focus, restarting speech)</li>
    +068 *       <li>Integration across multiple assistive technologies (magnification, braille, OCR)</li>
    +069 *       <li>Accessibility API awareness (UI Automation, MSAA, IAccessible2)</li>
    +070 *       <li>Settings management (profiles, application-specific configurations)</li>
    +071 *       <li>Profile creation and switching for different workflows/applications</li>
    +072 *       <li>Accessing vendor support resources and community forums</li>
    +073 *     </ul>
    +074 *   </li>
    +075 * </ul>
    +076 *
    +077 * <p><b>Data Persistence and Report Generation:</b></p>
    +078 * <ul>
    +079 *   <li>Scores captured via {@link com.studentgui.uicomp.PhaseScoreField} components (integer 0–4 typical)</li>
    +080 *   <li>Persisted to normalized schema via {@link com.studentgui.apphelpers.Database#insertAssessmentResults}</li>
    +081 *   <li>JSON export: {@code StudentDataFiles/<student>/Sessions/ScreenReader/ScreenReader-<sessionId>-<timestamp>.json}</li>
    +082 *   <li>Phase-grouped time-series PNG plots: {@code plots/ScreenReader-<sessionId>-<date>-P<N>.png} (4 phase groups)</li>
    +083 *   <li>Markdown report: {@code reports/ScreenReader-<sessionId>-<date>.md} with relative image links</li>
    +084 *   <li>HTML report: {@code reports/ScreenReader-<sessionId>-<date>.html} with inline styles and legends</li>
    +085 * </ul>
    +086 *
    +087 * <p>The shared {@link JLineGraph} visualizes recent session trends with phase-based grouping.
    +088 * Implements {@link com.studentgui.app.DateChangeListener} and {@link com.studentgui.app.StudentChangeListener}
    +089 * for dynamic refresh when global selections change.</p>
    +090 *
    +091 * @see com.studentgui.apphelpers.Database
    +092 * @see JLineGraph
    +093 * @see com.studentgui.uicomp.PhaseScoreField
    +094 */
    +095public class ScreenReader extends JPanel implements com.studentgui.app.DateChangeListener, com.studentgui.app.StudentChangeListener {
    +096    private static final Logger LOG = LoggerFactory.getLogger(ScreenReader.class);
    +097    /** Array of input fields corresponding to ScreenReader assessment parts. */
    +098    private final com.studentgui.uicomp.PhaseScoreField[] skillFields;
    +099    /** Canonical parts (code + label) for ScreenReader. */
    +100    private final String[][] parts;
    +101
    +102    /** Shared graph component used to visualize recent ScreenReader sessions. */
    +103    private final JLineGraph lineGraph;
    +104
    +105    /** Selected student's display name used for saves and plots (may be null). */
    +106    private String studentNameParam;
    +107    /** Title label shown at the top of the page. */
    +108    private JLabel titleLabel;
    +109    /** Base title used for the Screen Reader page header; date is appended when shown. */
    +110    private final String baseTitle = "Screen Reader Skills Progression";
    +111
    +112    /** Session date associated with entries made on this page. */
    +113    private LocalDate dateParam;
    +114
    +115    /**
    +116     * Construct a ScreenReader page bound to a student and date.
    +117     * The provided JLineGraph is used to render recent assessment results.
    +118     *
    +119     * @param studentName the student display name (may be null to indicate no selection)
    +120     * @param date        the date associated with the session
    +121     * @param lineGraph   chart component used to display recent results
    +122     */
    +123    public ScreenReader(String studentName, LocalDate date, JLineGraph lineGraph) {
    +124    this.studentNameParam = (studentName == null || studentName.trim().isEmpty()) ? com.studentgui.apphelpers.Helpers.defaultStudent() : studentName;
    +125        this.dateParam = date;
    +126        this.lineGraph = lineGraph;
    +127        setLayout(new BorderLayout());
    +128
    +129        this.parts = new String[][]{
    +130            {"P1_1","1.1 Basic Navigation"},{"P1_2","1.2 Read Labels"},{"P1_3","1.3 Interact Controls"},{"P1_4","1.4 Form Entry"},{"P1_5","1.5 Table Navigation"},{"P1_6","1.6 Headings"},
    +131            {"P2_1","2.1 Links"},{"P2_2","2.2 Lists"},{"P2_3","2.3 Images"},{"P2_4","2.4 Annotations"},
    +132            {"P3_1","3.1 Document Structure"},{"P3_2","3.2 Styles"},{"P3_3","3.3 Tables"},{"P3_4","3.4 Charts"},{"P3_5","3.5 Advanced Shortcuts"},{"P3_6","3.6 Scripting"},{"P3_7","3.7 Third Party Apps"},{"P3_8","3.8 Multimedia"},{"P3_9","3.9 Braille Display Use"},{"P3_10","3.10 Braille Tables"},{"P3_11","3.11Customization"},
    +133            {"P4_1","4.1 Performance"},{"P4_2","4.2 Error Recovery"},{"P4_3","4.3 Integration"},{"P4_4","4.4 Accessibility APIs"},{"P4_5","4.5 Settings"},{"P4_6","4.6 Profiles"},{"P4_7","4.7 Support"}
    +134        };
    +135
    +136    JPanel dataEntryPanel = new JPanel(new GridBagLayout());
    +137    JPanel view = new JPanel(new BorderLayout());
    +138    view.add(dataEntryPanel, BorderLayout.NORTH);
    +139    view.setBorder(javax.swing.BorderFactory.createEmptyBorder(20,20,20,20));
    +140    JScrollPane scroll = new JScrollPane(view);
    +141
    +142    GridBagConstraints gbc = new GridBagConstraints();
    +143    // tighter insets to keep rows within 1-2 lines vertical spacing
    +144    gbc.insets = new Insets(2,2,2,2);
    +145    gbc.fill = GridBagConstraints.HORIZONTAL;
    +146    gbc.anchor = GridBagConstraints.NORTHWEST; // left-align content
    +147    gbc.weightx = 1.0; // allow fields to take available width
    +148
    +149    this.titleLabel = new JLabel(baseTitle);
    +150    this.titleLabel.getAccessibleContext().setAccessibleName("Screen Reader Skills Progression Title");
    +151        // explicit title font for LAF-independence
    +152            this.titleLabel.setFont(new java.awt.Font(java.awt.Font.SANS_SERIF, Font.BOLD, 16));
    +153        gbc.gridx = 0; gbc.gridy = 0; gbc.gridwidth = GridBagConstraints.REMAINDER;
    +154        dataEntryPanel.add(this.titleLabel, gbc);
    +155
    +156    // compute label width using the PhaseScoreField label font (12pt) so wrapping is stable across themes
    +157    java.awt.Font labelFont = new java.awt.Font(java.awt.Font.SANS_SERIF, java.awt.Font.PLAIN, 12);
    +158    String[] labels = java.util.Arrays.stream(parts).map(x->x[1]).toArray(String[]::new);
    +159    int maxPx = com.studentgui.uicomp.PhaseScoreField.computeMaxLabelPixelWidth(labelFont, labels);
    +160    // clamp wider so most labels stay on 1-2 lines (200..360 px)
    +161    com.studentgui.uicomp.PhaseScoreField.setGlobalLabelWidth(Math.min(360, Math.max(200, maxPx + 50)));
    +162    skillFields = new com.studentgui.uicomp.PhaseScoreField[this.parts.length];
    +163        for (int i = 0; i < this.parts.length; i++) {
    +164            gbc.gridy = i + 1;
    +165            gbc.gridwidth = 2;
    +166            gbc.gridx = 0;
    +167            com.studentgui.uicomp.PhaseScoreField f = new com.studentgui.uicomp.PhaseScoreField(this.parts[i][1], 0);
    +168            f.setName("screenreader_" + this.parts[i][0]);
    +169            f.getAccessibleContext().setAccessibleName(this.parts[i][1]);
    +170            f.setToolTipText("Enter a numeric score for " + this.parts[i][1]);
    +171            skillFields[i] = f;
    +172            dataEntryPanel.add(f, gbc);
    +173        }
    +174
    +175    gbc.gridy = this.parts.length + 2;
    +176    gbc.weighty = 0.0;
    +177    gbc.gridx = 0;
    +178    gbc.gridwidth = 1;
    +179    gbc.anchor = GridBagConstraints.WEST;
    +180    JButton submit = new JButton("Submit Data");
    +181    submit.setPreferredSize(new java.awt.Dimension(0, 32));
    +182    submit.addActionListener((ActionEvent e) -> { submitData(); refreshGraph(); });
    +183    submit.setMnemonic(KeyEvent.VK_S);
    +184    submit.setToolTipText("Save ScreenReader scores for the selected student (Alt+S)");
    +185    submit.getAccessibleContext().setAccessibleName("Submit ScreenReader Data");
    +186    dataEntryPanel.add(submit, gbc);
    +187
    +188    gbc.gridx = 1;
    +189    JButton openLatest = new JButton("Open Latest Plot");
    +190    openLatest.setPreferredSize(new java.awt.Dimension(0, 32));
    +191    openLatest.addActionListener((ActionEvent e) -> openLatestPlot());
    +192    openLatest.setToolTipText("Open the most recently saved ScreenReader plot for this student");
    +193    openLatest.getAccessibleContext().setAccessibleName("Open Latest ScreenReader Plot");
    +194    dataEntryPanel.add(openLatest, gbc);
    +195
    +196    // consume remaining columns so layout stays compact and buttons are not clipped
    +197    gbc.gridx = 2; gbc.gridwidth = GridBagConstraints.REMAINDER; gbc.anchor = GridBagConstraints.WEST;
    +198    dataEntryPanel.add(new JPanel(), gbc);
    +199
    +200    scroll.getAccessibleContext().setAccessibleName("ScreenReader data entry scroll pane");
    +201    add(scroll, BorderLayout.CENTER);
    +202
    +203    SwingUtilities.invokeLater(() -> { view.setPreferredSize(view.getPreferredSize()); scroll.getViewport().setViewPosition(new java.awt.Point(0,0)); updateTitleDate(); revalidate(); });
    +204    // Diagnostic: log spinner positions and actual gap after layout
    +205    SwingUtilities.invokeLater(() -> {
    +206        for (com.studentgui.uicomp.PhaseScoreField f : skillFields) {
    +207            if (f != null) {
    +208                LOG.debug("ScreenReader field {} labelWidth={} spinnerX={} gap={}", f.getLabel(), f.getLabelWrapWidth(), f.getSpinnerX(), f.getActualGap());
    +209            }
    +210        }
    +211    });
    +212
    +213        com.studentgui.apphelpers.Helpers.createFolderHierarchy();
    +214        initDatabase();
    +215        // Do not refresh or save graphs automatically on construction to avoid
    +216        // writing files or opening images during application startup.
    +217        // refreshGraph();
    +218    }
    +219
    +220    /**
    +221     * Ensure the ScreenReader progress type and its assessment parts exist.
    +222     * This is idempotent and safe to call on page creation.
    +223     */
    +224    private void initDatabase() {
    +225        try {
    +226            int ptId = com.studentgui.apphelpers.Database.getOrCreateProgressType("ScreenReader");
    +227            String[] codes = new String[]{
    +228                "P1_1","P1_2","P1_3","P1_4","P1_5","P1_6",
    +229                "P2_1","P2_2","P2_3","P2_4",
    +230                "P3_1","P3_2","P3_3","P3_4","P3_5","P3_6","P3_7","P3_8","P3_9","P3_10","P3_11",
    +231                "P4_1","P4_2","P4_3","P4_4","P4_5","P4_6","P4_7"
    +232            };
    +233            com.studentgui.apphelpers.Database.ensureAssessmentParts(ptId, codes);
    +234        } catch (SQLException ex) {
    +235            LOG.error("Error initializing ScreenReader parts", ex);
    +236        }
    +237    }
    +238
    +239    /**
    +240     * Collect values from the entry fields, validate them, and persist
    +241     * them to the database as an assessment session.
    +242     */
    +243    private void submitData() {
    +244        if (this.studentNameParam == null || this.studentNameParam.trim().isEmpty()) {
    +245            JOptionPane.showMessageDialog(this, "Please select a student before submitting ScreenReader data.", "Missing student", JOptionPane.WARNING_MESSAGE);
    +246            return;
    +247        }
    +248        try {
    +249            int studentId = com.studentgui.apphelpers.Database.getOrCreateStudent(this.studentNameParam);
    +250            int ptId = com.studentgui.apphelpers.Database.getOrCreateProgressType("ScreenReader");
    +251            int sessionId = com.studentgui.apphelpers.Database.createProgressSession(studentId, ptId, this.dateParam);
    +252            String[] codes = new String[this.parts.length];
    +253            int[] scores = new int[this.parts.length];
    +254            for (int i = 0; i < this.parts.length; i++) {
    +255                codes[i] = this.parts[i][0];
    +256                scores[i] = skillFields[i].getValue();
    +257            }
    +258            com.studentgui.apphelpers.Database.insertAssessmentResults(sessionId, ptId, codes, scores);
    +259            LOG.info("ScreenReader data submitted for student={}", this.studentNameParam);
    +260            com.studentgui.apphelpers.UiNotifier.show("ScreenReader data saved.");
    +261            com.studentgui.apphelpers.dto.AssessmentPayload payload = new com.studentgui.apphelpers.dto.AssessmentPayload(sessionId, codes, scores);
    +262            java.nio.file.Path jsonOut = com.studentgui.apphelpers.SessionJsonWriter.writeSessionJson(this.studentNameParam, "ScreenReader", payload, sessionId);
    +263                if (jsonOut == null) {
    +264                    LOG.warn("Unable to save ScreenReader session JSON for sessionId={}", sessionId);
    +265                }
    +266            try {
    +267                java.nio.file.Path plotsOut = com.studentgui.apphelpers.Helpers.studentPlotsDir(this.studentNameParam);
    +268                java.nio.file.Path reportsOut = com.studentgui.apphelpers.Helpers.studentReportsDir(this.studentNameParam);
    +269                java.nio.file.Files.createDirectories(plotsOut);
    +270                java.nio.file.Files.createDirectories(reportsOut);
    +271                java.time.format.DateTimeFormatter df = java.time.format.DateTimeFormatter.ISO_DATE;
    +272                String dateStr = this.dateParam != null ? this.dateParam.format(df) : java.time.LocalDate.now().toString();
    +273                String baseName = "ScreenReader-" + sessionId + "-" + dateStr;
    +274
    +275                com.studentgui.apphelpers.Database.ResultsWithDates rwd = com.studentgui.apphelpers.Database.fetchLatestAssessmentResultsWithDates(this.studentNameParam, "ScreenReader", Integer.MAX_VALUE);
    +276                java.util.Map<String, java.nio.file.Path> groups = null;
    +277                String[] labels = new String[this.parts.length];
    +278                for (int i = 0; i < this.parts.length; i++) {
    +279                    labels[i] = this.parts[i][1];
    +280                }
    +281                if (rwd != null && rwd.rows != null && !rwd.rows.isEmpty()) {
    +282                    lineGraph.updateWithGroupedDataByDate(rwd.dates, rwd.rows, codes, labels);
    +283                    groups = lineGraph.saveGroupedCharts(plotsOut, baseName, 1000, 240);
    +284                    java.time.LocalDate headerDate = rwd.dates.get(rwd.dates.size() - 1);
    +285                    dateStr = headerDate.format(df);
    +286                } else {
    +287                    java.util.List<java.util.List<Integer>> rowsList = new java.util.ArrayList<>();
    +288                    java.util.List<Integer> latest = new java.util.ArrayList<>();
    +289                    for (int v : scores) {
    +290                        latest.add(v);
    +291                    }
    +292                    rowsList.add(latest);
    +293                    lineGraph.updateWithGroupedData(rowsList, codes);
    +294                    groups = lineGraph.saveGroupedCharts(plotsOut, baseName, 1000, 240);
    +295                }
    +296
    +297                if (groups == null) {
    +298                    groups = new java.util.LinkedHashMap<>();
    +299                }
    +300                StringBuilder md = new StringBuilder();
    +301                md.append("# ").append(this.studentNameParam == null ? "Unknown Student" : this.studentNameParam).append(" - ").append(dateStr).append("\n\n");
    +302                for (java.util.Map.Entry<String, java.nio.file.Path> e : groups.entrySet()) {
    +303                    md.append("## ").append(e.getKey()).append("\n\n");
    +304                    md.append("![](../plots/").append(e.getValue().getFileName().toString()).append(")\n\n");
    +305                }
    +306                java.nio.file.Path mdFile = reportsOut.resolve(baseName + ".md");
    +307                java.nio.file.Files.writeString(mdFile, md.toString(), java.nio.charset.StandardCharsets.UTF_8);
    +308
    +309                // HTML using shared palette
    +310                try {
    +311                    String[] palette = JLineGraph.PALETTE_HEX;
    +312                    java.util.LinkedHashMap<String, java.util.List<Integer>> groupsIdx = new java.util.LinkedHashMap<>();
    +313                    for (int i = 0; i < codes.length; i++) {
    +314                        String code = codes[i];
    +315                        String grp = code != null && code.contains("_") ? code.split("_")[0] : code;
    +316                        groupsIdx.computeIfAbsent(grp, k -> new java.util.ArrayList<>()).add(i);
    +317                    }
    +318                    StringBuilder html = new StringBuilder();
    +319                    html.append("<!doctype html><html><head><meta charset=\"utf-8\"><title>");
    +320                    html.append(this.studentNameParam == null ? "Student Report" : this.studentNameParam).append(" - ").append(dateStr).append("</title>");
    +321                    html.append("<style>body{font-family:sans-serif;margin:20px;} img{max-width:100%;height:auto;border:1px solid #ccc;margin-bottom:8px;} .legend{max-height:160px;overflow:auto;border:1px solid #ddd;padding:8px;margin-bottom:24px;} .legend-item{display:flex;align-items:center;gap:8px;padding:4px 0;} .swatch{width:18px;height:12px;border:1px solid #333;display:inline-block}</style>");
    +322                    html.append("</head><body>");
    +323                    html.append("<h1>").append(this.studentNameParam == null ? "Unknown Student" : this.studentNameParam).append(" - ").append(dateStr).append("</h1>");
    +324                    for (java.util.Map.Entry<String, java.nio.file.Path> e2 : groups.entrySet()) {
    +325                        String grp = e2.getKey();
    +326                        String imgName = e2.getValue().getFileName().toString();
    +327                        html.append("<h2>").append(grp).append("</h2>");
    +328                        html.append("<div class=\"plot\"><img src=\"../plots/").append(imgName).append("\" alt=\"").append(grp).append("\"></div>");
    +329                        java.util.List<Integer> idxs = groupsIdx.getOrDefault(grp, new java.util.ArrayList<>());
    +330                        html.append("<div class=\"legend\">");
    +331                        for (int s = 0; s < idxs.size(); s++) {
    +332                            int idx = idxs.get(s);
    +333                            String code = codes[idx];
    +334                            String human = this.parts[idx][1];
    +335                            String seriesName = code + " - " + human;
    +336                            String color = palette[s % palette.length];
    +337                            html.append("<div class=\"legend-item\">");
    +338                            html.append("<span class=\"swatch\" style=\"background:");
    +339                            html.append(color);
    +340                            html.append(";\"></span>");
    +341                            html.append("<div>");
    +342                            html.append(seriesName);
    +343                            html.append("</div></div>");
    +344                        }
    +345                        html.append("</div>");
    +346                    }
    +347                    html.append("</body></html>");
    +348                    java.nio.file.Path htmlFile = reportsOut.resolve(baseName + ".html");
    +349                    java.nio.file.Files.writeString(htmlFile, html.toString(), java.nio.charset.StandardCharsets.UTF_8);
    +350                    LOG.info("Wrote ScreenReader HTML session report {}", htmlFile);
    +351                } catch (java.io.IOException ioex) {
    +352                    LOG.warn("Unable to write ScreenReader HTML report: {}", ioex.toString());
    +353                }
    +354
    +355                LOG.info("Wrote ScreenReader session report {} with {} group images", mdFile, groups.size());
    +356            } catch (java.io.IOException | SQLException ex) {
    +357                LOG.warn("Unable to save ScreenReader per-phase plots or markdown report: {}", ex.toString());
    +358            }
    +359        } catch (NumberFormatException ex) {
    +360            LOG.warn("Invalid number in skill fields", ex);
    +361        } catch (SQLException ex) {
    +362            LOG.error("DB error submitting ScreenReader data", ex);
    +363            JOptionPane.showMessageDialog(this, "Database error saving ScreenReader data: " + ex.getMessage(), "Database error", JOptionPane.ERROR_MESSAGE);
    +364        }
    +365    }
    +366
    +367    /**
    +368     * Refresh the attached JLineGraph with the latest ScreenReader data for
    +369     * the configured student.
    +370     */
    +371    private void refreshGraph() {
    +372        try {
    +373            List<List<Integer>> allSkillValues = com.studentgui.apphelpers.Database.fetchLatestAssessmentResults(studentNameParam, "ScreenReader", 5);
    +374            if (allSkillValues != null && !allSkillValues.isEmpty()) {
    +375                String[] codes = new String[this.parts.length];
    +376                    for (int i = 0; i < this.parts.length; i++) {
    +377                        codes[i] = this.parts[i][0];
    +378                    }
    +379                lineGraph.updateWithGroupedData(allSkillValues, codes);
    +380                LOG.info("Graph updated with {} series", allSkillValues.size());
    +381            } else {
    +382                LOG.info("No ScreenReader data to plot for {}", studentNameParam);
    +383            }
    +384        } catch (SQLException ex) {
    +385            LOG.error("Error fetching ScreenReader data", ex);
    +386        }
    +387
    +388        // Do not save chart images during refresh to avoid creating files on app startup.
    +389        LOG.debug("Skipping auto-save of ScreenReader chart during refresh for student={}", this.studentNameParam);
    +390    }
    +391
    +392    @Override
    +393    public void dateChanged(LocalDate newDate) {
    +394        this.dateParam = newDate;
    +395        SwingUtilities.invokeLater(() -> {
    +396            refreshGraph();
    +397            updateTitleDate();
    +398        });
    +399    }
    +400
    +401    @Override
    +402    public void studentChanged(String newStudent) {
    +403        this.studentNameParam = newStudent;
    +404        SwingUtilities.invokeLater(() -> {
    +405            refreshGraph();
    +406            updateTitleDate();
    +407        });
    +408    }
    +409
    +410    private void updateTitleDate() {
    +411        try {
    +412            String dateStr = this.dateParam != null ? this.dateParam.toString() : java.time.LocalDate.now().toString();
    +413            this.titleLabel.setText(baseTitle + " - " + dateStr);
    +414        } catch (Exception ex) {
    +415            this.titleLabel.setText(baseTitle);
    +416        }
    +417    }
    +418
    +419    private void openLatestPlot() {
    +420        java.nio.file.Path p = com.studentgui.apphelpers.Helpers.latestPlotPath(this.studentNameParam, "ScreenReader");
    +421        if (p == null) {
    +422            com.studentgui.apphelpers.UiNotifier.show("No ScreenReader plot found for student");
    +423            return;
    +424        }
    +425    try { java.awt.Desktop.getDesktop().open(p.toFile()); }
    +426    catch (java.io.IOException | UnsupportedOperationException | SecurityException ex) { com.studentgui.apphelpers.UiNotifier.show("Unable to open plot: " + p.getFileName().toString()); }
    +427    }
    +428
    +429}
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    + + diff --git a/target/site/apidocs/src-html/com/studentgui/apppages/SessionNotes.html b/target/site/apidocs/src-html/com/studentgui/apppages/SessionNotes.html new file mode 100644 index 0000000..3e17625 --- /dev/null +++ b/target/site/apidocs/src-html/com/studentgui/apppages/SessionNotes.html @@ -0,0 +1,216 @@ + + + + +Source code + + + + + + +
    +
    +
    001package com.studentgui.apppages;
    +002
    +003import java.awt.BorderLayout;
    +004import java.awt.Font;
    +005import java.awt.GridBagConstraints;
    +006import java.awt.GridBagLayout;
    +007import java.awt.Insets;
    +008import java.awt.event.ActionEvent;
    +009import java.awt.event.KeyEvent;
    +010import java.sql.SQLException;
    +011import java.time.LocalDate;
    +012
    +013import javax.swing.JButton;
    +014import javax.swing.JLabel;
    +015import javax.swing.JPanel;
    +016import javax.swing.JScrollPane;
    +017import javax.swing.JTextArea;
    +018import javax.swing.SwingUtilities;
    +019
    +020import org.slf4j.Logger;
    +021import org.slf4j.LoggerFactory;
    +022
    +023/**
    +024 * Freeform session notes editor for general observations and reflections.
    +025 *
    +026 * <p>Provides a simple multi-line text area for educators to record unstructured notes
    +027 * about a student session. This complements the structured assessment pages (Braille, Abacus, etc.)
    +028 * by allowing qualitative observations, anecdotal records, and contextual details that don't
    +029 * fit into numeric scoring fields.</p>
    +030 *
    +031 * <p><b>Typical Use Cases:</b></p>
    +032 * <ul>
    +033 *   <li>Recording behavioral observations (e.g., "Student showed increased frustration with Nemeth fractions today")</li>
    +034 *   <li>Documenting environmental factors affecting performance (e.g., "Noisy classroom due to construction")</li>
    +035 *   <li>Noting equipment issues or accommodations used (e.g., "Switched to Braille Sense due to BrailleNote malfunction")</li>
    +036 *   <li>General reflections or instructional notes for future reference</li>
    +037 * </ul>
    +038 *
    +039 * <p><b>Data Storage:</b></p>
    +040 * <ul>
    +041 *   <li>Notes persisted via {@link com.studentgui.apphelpers.Database#saveSessionNotes} to {@code ProgressSession.notes} column</li>
    +042 *   <li>Associated with a SessionNotes progress type and session ID for consistent querying</li>
    +043 *   <li>JSON export: {@code StudentDataFiles/<student>/Sessions/SessionNotes/SessionNotes-<sessionId>-<timestamp>.json}</li>
    +044 *   <li>No plots or reports generated (text-only data)</li>
    +045 * </ul>
    +046 *
    +047 * <p>The shared {@link JLineGraph} component is present for UI layout consistency but remains
    +048 * empty (session notes are not quantitative data). This page does not implement listener interfaces
    +049 * as it operates on static student/date parameters provided at construction time.</p>
    +050 *
    +051 * @see com.studentgui.apphelpers.Database#saveSessionNotes
    +052 * @see com.studentgui.apphelpers.dto.NotesPayload
    +053 */
    +054public class SessionNotes extends JPanel {
    +055    private static final Logger LOG = LoggerFactory.getLogger(SessionNotes.class);
    +056    /** Text area containing session notes entered by the user. */
    +057    private final JTextArea notesArea;
    +058
    +059    /** Selected student's display name used when saving session notes (may be null). */
    +060    private final String studentNameParam;
    +061
    +062    /** Date associated with these session notes. */
    +063    private final LocalDate dateParam;
    +064
    +065    /**
    +066     * Create a SessionNotes page for the provided student and date.
    +067     * The supplied JLineGraph is displayed below the notes editor.
    +068     *
    +069     * @param studentName student display name (may be null when no student selected)
    +070     * @param date        the date this session pertains to
    +071     * @param graph       the chart component shown beneath the notes
    +072     */
    +073    public SessionNotes(String studentName, LocalDate date, JLineGraph graph) {
    +074    this.studentNameParam = (studentName == null || studentName.trim().isEmpty()) ? com.studentgui.apphelpers.Helpers.defaultStudent() : studentName;
    +075        this.dateParam = date;
    +076        setLayout(new BorderLayout());
    +077
    +078    JPanel p = new JPanel(new GridBagLayout());
    +079    JPanel view = new JPanel(new BorderLayout());
    +080    view.add(p, BorderLayout.NORTH);
    +081    view.setBorder(javax.swing.BorderFactory.createEmptyBorder(20,20,20,20));
    +082    JScrollPane scroll = new JScrollPane(view);
    +083    scroll.getAccessibleContext().setAccessibleName("Session Notes data entry scroll pane");
    +084        GridBagConstraints gbc = new GridBagConstraints(); gbc.insets=new Insets(2,2,2,2); gbc.fill = GridBagConstraints.BOTH; gbc.anchor = GridBagConstraints.NORTHWEST;
    +085    JLabel title = new JLabel("Session Notes", JLabel.LEFT);
    +086    title.setFont(new Font(Font.SANS_SERIF, Font.BOLD, 16));
    +087    title.getAccessibleContext().setAccessibleName("Session Notes Title");
    +088    gbc.gridx=0; gbc.gridy=0; gbc.gridwidth=1; p.add(title, gbc);
    +089
    +090    int globalLabel = com.studentgui.uicomp.PhaseScoreField.getGlobalLabelWidth();
    +091    gbc.gridy=1; gbc.gridx=0; JLabel notesLabel = new JLabel("Notes:"); notesLabel.setPreferredSize(new java.awt.Dimension(globalLabel, notesLabel.getPreferredSize().height)); p.add(notesLabel, gbc);
    +092    gbc.gridy=2; gbc.gridx=0; notesArea = new JTextArea(8,40); notesArea.setLineWrap(true); notesArea.setWrapStyleWord(true); notesArea.setToolTipText("Enter session notes for the student"); notesArea.getAccessibleContext().setAccessibleName("Session notes"); p.add(notesArea, gbc);
    +093    notesLabel.setLabelFor(notesArea);
    +094
    +095    gbc.gridy=3; JButton submit = new JButton("Save Session Notes");
    +096    submit.addActionListener((ActionEvent e)-> saveNotes());
    +097    submit.setMnemonic(KeyEvent.VK_S);
    +098    submit.setToolTipText("Save session notes (Alt+S)");
    +099    submit.getAccessibleContext().setAccessibleName("Save Session Notes");
    +100    p.add(submit, gbc);
    +101
    +102        add(scroll, BorderLayout.CENTER);
    +103
    +104        SwingUtilities.invokeLater(()->{ p.setPreferredSize(p.getPreferredSize()); revalidate(); });
    +105
    +106        com.studentgui.apphelpers.Helpers.createFolderHierarchy();
    +107    }
    +108
    +109    /**
    +110     * Persist the contents of the session notes into the database. Ensures
    +111     * required student and progress session records exist and writes the notes
    +112     * to the ProgressSession.notes column.
    +113     */
    +114    private void saveNotes() {
    +115        if (this.studentNameParam == null || this.studentNameParam.trim().isEmpty()) {
    +116            javax.swing.JOptionPane.showMessageDialog(this, "Please select a student before saving session notes.", "Missing student", javax.swing.JOptionPane.WARNING_MESSAGE);
    +117            return;
    +118        }
    +119
    +120        try {
    +121            int studentId = com.studentgui.apphelpers.Database.getOrCreateStudent(this.studentNameParam);
    +122            int ptId = com.studentgui.apphelpers.Database.getOrCreateProgressType("SessionNotes");
    +123            int sessionId = com.studentgui.apphelpers.Database.createProgressSession(studentId, ptId, this.dateParam);
    +124            String notes = notesArea.getText();
    +125            com.studentgui.apphelpers.Database.saveSessionNotes(sessionId, notes);
    +126            LOG.info("Saved session notes for {}", studentNameParam);
    +127            com.studentgui.apphelpers.UiNotifier.show("Session notes saved.");
    +128            com.studentgui.apphelpers.dto.NotesPayload payload = new com.studentgui.apphelpers.dto.NotesPayload(sessionId, notes);
    +129            java.nio.file.Path jsonOut = com.studentgui.apphelpers.SessionJsonWriter.writeSessionJson(this.studentNameParam, "SessionNotes", payload, sessionId);
    +130            if (jsonOut == null) {
    +131                LOG.warn("Unable to save SessionNotes session JSON for sessionId={}", sessionId);
    +132            }
    +133        } catch (SQLException ex) {
    +134            LOG.error("Error saving session notes", ex);
    +135            javax.swing.JOptionPane.showMessageDialog(this, "Database error saving session notes: " + ex.getMessage(), "Database error", javax.swing.JOptionPane.ERROR_MESSAGE);
    +136        }
    +137    }
    +138}
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    + + diff --git a/target/site/apidocs/src-html/com/studentgui/apptheming/Theme.html b/target/site/apidocs/src-html/com/studentgui/apptheming/Theme.html new file mode 100644 index 0000000..73f47a6 --- /dev/null +++ b/target/site/apidocs/src-html/com/studentgui/apptheming/Theme.html @@ -0,0 +1,517 @@ + + + + +Source code + + + + + + +
    +
    +
    001package com.studentgui.apptheming;
    +002
    +003import java.awt.Color;
    +004import java.awt.Graphics2D;
    +005import java.awt.RenderingHints;
    +006import java.awt.event.ActionEvent;
    +007import java.awt.event.InputEvent;
    +008import java.awt.event.KeyEvent;
    +009import java.awt.image.BufferedImage;
    +010import java.io.File;
    +011import java.io.IOException;
    +012import java.net.JarURLConnection;
    +013import java.net.URI;
    +014import java.net.URL;
    +015import java.net.URLConnection;
    +016import java.util.ArrayList;
    +017import java.util.Enumeration;
    +018import java.util.List;
    +019import java.util.jar.JarEntry;
    +020import java.util.jar.JarFile;
    +021
    +022import javax.swing.AbstractAction;
    +023import javax.swing.ImageIcon;
    +024import javax.swing.JMenu;
    +025import javax.swing.JMenuBar;
    +026import javax.swing.JMenuItem;
    +027import javax.swing.KeyStroke;
    +028
    +029import com.studentgui.app.Main;
    +030
    +031/**
    +032 * Application theming and menu bar construction utilities.
    +033 *
    +034 * <p>Provides centralized menu bar factory for the main application window with
    +035 * keyboard shortcuts, mnemonics, and accessibility support. The menu structure
    +036 * organizes assessment pages into logical categories:</p>
    +037 *
    +038 * <ul>
    +039 *   <li><b>Navigate Menu:</b> Primary navigation menu containing:
    +040 *     <ul>
    +041 *       <li><b>Home:</b> Returns to homepage (Ctrl+Alt+H)</li>
    +042 *       <li><b>Tactile Submenu:</b> Braille and Abacus skills pages (alphabetical)</li>
    +043 *       <li><b>Technology Submenu:</b> Device-specific pages (BrailleNote, BrailleSense, iOS, ScreenReader, etc.)</li>
    +044 *       <li><b>Communication Submenu:</b> Contact Log and Session Notes</li>
    +045 *       <li><b>Other Skills Submenu:</b> CVI, Digital Literacy, Keyboarding, Observations, Instructional Materials</li>
    +046 *     </ul>
    +047 *   </li>
    +048 * </ul>
    +049 *
    +050 * <p><b>Accessibility Features:</b></p>
    +051 * <ul>
    +052 *   <li>All menu items include accessible names and descriptions</li>
    +053 *   <li>Keyboard shortcuts use Ctrl+Alt+Letter combinations to avoid conflicts</li>
    +054 *   <li>Mnemonics provided for primary menu items (Alt+H for Home, etc.)</li>
    +055 *   <li>Color-coded icons generated programmatically via {@link #makeIcon(Color, int)}</li>
    +056 * </ul>
    +057 *
    +058 * <p><b>Icon Generation:</b> Menu items display small colored square icons for
    +059 * visual differentiation. Icons are generated at runtime as 12×12px {@link BufferedImage}
    +060 * instances with anti-aliased rendering for smooth appearance across themes.</p>
    +061 *
    +062 * <p><b>Menu Structure Rationale:</b></p>
    +063 * <ul>
    +064 *   <li>Tactile skills (Braille, Abacus) grouped separately from technology devices</li>
    +065 *   <li>Technology submenu organized by device type (notetakers, mobile OS, desktop screen readers)</li>
    +066 *   <li>Communication tools (Contact Log, Session Notes) kept together for workflow consistency</li>
    +067 *   <li>Remaining assessment pages grouped under "Other Skills" for flexibility</li>
    +068 * </ul>
    +069 *
    +070 * <p><b>Navigation Integration:</b> All menu items invoke the main navigation logic in {@link com.studentgui.app.Main}
    +071 * to switch the main content panel. Page identifiers are lowercase strings matching page class names
    +072 * (e.g., "braille", "abacus", "braillenote").</p>
    +073 *
    +074 * <p><b>Theme Management:</b> Currently limited to menu bar construction. Future expansion
    +075 * may include FlatLaf theme switching, custom color schemes, or icon set selection.</p>
    +076 *
    +077 * @see com.studentgui.app.Main
    +078 * @see javax.swing.JMenuBar
    +079 * @see javax.swing.JMenu
    +080 * @see javax.swing.JMenuItem
    +081 */
    +082public class Theme {
    +083    /**
    +084     * Build and return the application menu bar used in the main frame.
    +085     *
    +086     * @return a {@link JMenuBar} instance containing the application's menus
    +087     */
    +088    public static JMenuBar createMenuBar() {
    +089        JMenuBar mb = new JMenuBar();
    +090        JMenu nav = new JMenu("Navigate");
    +091
    +092        // Home
    +093        JMenuItem home = new JMenuItem(new AbstractAction("Home") {
    +094            @Override
    +095            public void actionPerformed(final ActionEvent e) { Main.showPage("homepage", null); }
    +096        });
    +097        home.setMnemonic('H');
    +098        home.setAccelerator(KeyStroke.getKeyStroke(KeyEvent.VK_H, InputEvent.CTRL_DOWN_MASK | InputEvent.ALT_DOWN_MASK));
    +099        home.setIcon(makeIcon(new Color(0x4A90E2), 12));
    +100        home.getAccessibleContext().setAccessibleName("Home");
    +101        home.getAccessibleContext().setAccessibleDescription("Open the Home page");
    +102        nav.add(home);
    +103        nav.addSeparator();
    +104
    +105        // Tactile section (alphabetical)
    +106        JMenu tactile = new JMenu("Tactile");
    +107        JMenuItem abacus = new JMenuItem(new AbstractAction("Abacus") {
    +108            @Override public void actionPerformed(final ActionEvent e) { Main.showPage("abacus", null); }
    +109        });
    +110        abacus.setAccelerator(KeyStroke.getKeyStroke(KeyEvent.VK_A, InputEvent.CTRL_DOWN_MASK | InputEvent.ALT_DOWN_MASK));
    +111        abacus.setIcon(makeIcon(new Color(0xF5A623), 12));
    +112        abacus.getAccessibleContext().setAccessibleName("Abacus");
    +113        abacus.getAccessibleContext().setAccessibleDescription("Open the Abacus skills page");
    +114        tactile.add(abacus);
    +115
    +116        JMenuItem braille = new JMenuItem(new AbstractAction("Braille") {
    +117            @Override public void actionPerformed(final ActionEvent e) { Main.showPage("braille", null); }
    +118        });
    +119        braille.setAccelerator(KeyStroke.getKeyStroke(KeyEvent.VK_B, InputEvent.CTRL_DOWN_MASK | InputEvent.ALT_DOWN_MASK));
    +120        braille.setIcon(makeIcon(new Color(0x50E3C2), 12));
    +121        braille.getAccessibleContext().setAccessibleName("Braille");
    +122        braille.getAccessibleContext().setAccessibleDescription("Open the Braille skills page");
    +123        tactile.add(braille);
    +124
    +125        nav.add(tactile);
    +126        nav.addSeparator();
    +127
    +128        // Technology section (alphabetical)
    +129        JMenu tech = new JMenu("Technology");
    +130        JMenuItem brailleNote = new JMenuItem(new AbstractAction("BrailleNote Touch") {
    +131            @Override public void actionPerformed(final ActionEvent e) { Main.showPage("braillenote", null); }
    +132        });
    +133        brailleNote.setAccelerator(KeyStroke.getKeyStroke(KeyEvent.VK_G, InputEvent.CTRL_DOWN_MASK | InputEvent.ALT_DOWN_MASK));
    +134        brailleNote.setIcon(makeIcon(new Color(0x7B61FF), 12));
    +135        brailleNote.getAccessibleContext().setAccessibleName("BrailleNote Touch");
    +136        brailleNote.getAccessibleContext().setAccessibleDescription("Open the BrailleNote Touch page");
    +137        tech.add(brailleNote);
    +138
    +139        JMenuItem brailleSense = new JMenuItem(new AbstractAction("Braille Sense") {
    +140            @Override public void actionPerformed(final ActionEvent e) { Main.showPage("braillesense", null); }
    +141        });
    +142        brailleSense.setAccelerator(KeyStroke.getKeyStroke(KeyEvent.VK_S, InputEvent.CTRL_DOWN_MASK | InputEvent.ALT_DOWN_MASK));
    +143        brailleSense.setIcon(makeIcon(new Color(0xF8E71C), 12));
    +144        brailleSense.getAccessibleContext().setAccessibleName("Braille Sense");
    +145        brailleSense.getAccessibleContext().setAccessibleDescription("Open the Braille Sense page");
    +146        tech.add(brailleSense);
    +147
    +148        JMenuItem dl = new JMenuItem(new AbstractAction("Digital Literacy") {
    +149            @Override public void actionPerformed(final ActionEvent e) { Main.showPage("digitalliteracy", null); }
    +150        });
    +151        dl.setAccelerator(KeyStroke.getKeyStroke(KeyEvent.VK_D, InputEvent.CTRL_DOWN_MASK | InputEvent.ALT_DOWN_MASK));
    +152        dl.setIcon(makeIcon(new Color(0x7ED321), 12));
    +153        dl.getAccessibleContext().setAccessibleName("Digital Literacy");
    +154        dl.getAccessibleContext().setAccessibleDescription("Open the Digital Literacy page");
    +155        tech.add(dl);
    +156
    +157        JMenuItem ios = new JMenuItem(new AbstractAction("iOS") {
    +158            @Override public void actionPerformed(final ActionEvent e) { Main.showPage("ios", null); }
    +159        });
    +160        ios.setAccelerator(KeyStroke.getKeyStroke(KeyEvent.VK_I, InputEvent.CTRL_DOWN_MASK | InputEvent.ALT_DOWN_MASK));
    +161        ios.setIcon(makeIcon(new Color(0x00A5E0), 12));
    +162        ios.getAccessibleContext().setAccessibleName("iOS");
    +163        ios.getAccessibleContext().setAccessibleDescription("Open the iOS accessibility page");
    +164        tech.add(ios);
    +165
    +166        JMenuItem keyboarding = new JMenuItem(new AbstractAction("Keyboarding") {
    +167            @Override public void actionPerformed(final ActionEvent e) { Main.showPage("keyboarding", null); }
    +168        });
    +169        keyboarding.setAccelerator(KeyStroke.getKeyStroke(KeyEvent.VK_K, InputEvent.CTRL_DOWN_MASK | InputEvent.ALT_DOWN_MASK));
    +170        keyboarding.setIcon(makeIcon(new Color(0x8B572A), 12));
    +171        keyboarding.getAccessibleContext().setAccessibleName("Keyboarding");
    +172        keyboarding.getAccessibleContext().setAccessibleDescription("Open the Keyboarding skills page");
    +173        tech.add(keyboarding);
    +174
    +175        JMenuItem screenReader = new JMenuItem(new AbstractAction("Screen Reader") {
    +176            @Override public void actionPerformed(final ActionEvent e) { Main.showPage("screenreader", null); }
    +177        });
    +178        screenReader.setAccelerator(KeyStroke.getKeyStroke(KeyEvent.VK_R, InputEvent.CTRL_DOWN_MASK | InputEvent.ALT_DOWN_MASK));
    +179        screenReader.setIcon(makeIcon(new Color(0x417505), 12));
    +180        screenReader.getAccessibleContext().setAccessibleName("Screen Reader");
    +181        screenReader.getAccessibleContext().setAccessibleDescription("Open the Screen Reader page");
    +182        tech.add(screenReader);
    +183
    +184        nav.add(tech);
    +185        nav.addSeparator();
    +186
    +187        // Misc (alphabetical)
    +188        JMenu misc = new JMenu("Misc");
    +189        JMenuItem contactLog = new JMenuItem(new AbstractAction("Contact Log") {
    +190            @Override public void actionPerformed(final ActionEvent e) { Main.showPage("contactlog", null); }
    +191        });
    +192        contactLog.setAccelerator(KeyStroke.getKeyStroke(KeyEvent.VK_C, InputEvent.CTRL_DOWN_MASK | InputEvent.ALT_DOWN_MASK));
    +193        contactLog.setIcon(makeIcon(new Color(0xF18805), 12));
    +194        contactLog.getAccessibleContext().setAccessibleName("Contact Log");
    +195        contactLog.getAccessibleContext().setAccessibleDescription("Open the Contact Log page");
    +196        misc.add(contactLog);
    +197
    +198        JMenuItem observations = new JMenuItem(new AbstractAction("Observations") {
    +199            @Override public void actionPerformed(final ActionEvent e) { Main.showPage("observations", null); }
    +200        });
    +201        observations.setAccelerator(KeyStroke.getKeyStroke(KeyEvent.VK_O, InputEvent.CTRL_DOWN_MASK | InputEvent.ALT_DOWN_MASK));
    +202        observations.setIcon(makeIcon(new Color(0x50E3C2), 12));
    +203        observations.getAccessibleContext().setAccessibleName("Observations");
    +204        observations.getAccessibleContext().setAccessibleDescription("Open the Observations page");
    +205        misc.add(observations);
    +206
    +207        JMenuItem sessionNotes = new JMenuItem(new AbstractAction("Session Notes") {
    +208            @Override public void actionPerformed(final ActionEvent e) { Main.showPage("sessionnotes", null); }
    +209        });
    +210        sessionNotes.setAccelerator(KeyStroke.getKeyStroke(KeyEvent.VK_N, InputEvent.CTRL_DOWN_MASK | InputEvent.ALT_DOWN_MASK));
    +211        sessionNotes.setIcon(makeIcon(new Color(0xD0021B), 12));
    +212        sessionNotes.getAccessibleContext().setAccessibleName("Session Notes");
    +213        sessionNotes.getAccessibleContext().setAccessibleDescription("Open the Session Notes page");
    +214        misc.add(sessionNotes);
    +215
    +216        nav.add(misc);
    +217
    +218        mb.add(nav);
    +219
    +220        // Themes menu (top-level)
    +221        JMenu themesMenu = new JMenu("Themes");
    +222        // Read persisted theme choice so we can mark the active menu item
    +223        String currentTheme = com.studentgui.apphelpers.Settings.get("theme", "light");
    +224
    +225        javax.swing.ButtonGroup themeGroup = new javax.swing.ButtonGroup();
    +226
    +227        javax.swing.JRadioButtonMenuItem light = new javax.swing.JRadioButtonMenuItem(new AbstractAction("Light") {
    +228            @Override public void actionPerformed(final ActionEvent e) { Main.setTheme("light"); com.studentgui.apphelpers.Settings.put("theme", "light"); }
    +229        });
    +230        light.setIcon(makeIcon(new Color(0x000000), 12));
    +231        light.getAccessibleContext().setAccessibleName("Light theme");
    +232        light.getAccessibleContext().setAccessibleDescription("Switch to the light theme");
    +233        if ("light".equalsIgnoreCase(currentTheme) || "flatlightlaf".equalsIgnoreCase(currentTheme)) {
    +234            light.setSelected(true);
    +235        }
    +236        themeGroup.add(light);
    +237        themesMenu.add(light);
    +238
    +239        javax.swing.JRadioButtonMenuItem dark = new javax.swing.JRadioButtonMenuItem(new AbstractAction("Dark") {
    +240            @Override public void actionPerformed(final ActionEvent e) { Main.setTheme("dark"); com.studentgui.apphelpers.Settings.put("theme", "dark"); }
    +241        });
    +242        dark.setIcon(makeIcon(new Color(0x2C2C2C), 12));
    +243        dark.getAccessibleContext().setAccessibleName("Dark theme");
    +244        dark.getAccessibleContext().setAccessibleDescription("Switch to the dark theme");
    +245        if ("dark".equalsIgnoreCase(currentTheme) || "flatdarklaf".equalsIgnoreCase(currentTheme)) {
    +246            dark.setSelected(true);
    +247        }
    +248        themeGroup.add(dark);
    +249        themesMenu.add(dark);
    +250
    +251        javax.swing.JRadioButtonMenuItem intellij = new javax.swing.JRadioButtonMenuItem(new AbstractAction("IntelliJ (Darcula)") {
    +252            @Override public void actionPerformed(final ActionEvent e) { Main.setTheme("darcula"); com.studentgui.apphelpers.Settings.put("theme", "darcula"); }
    +253        });
    +254        intellij.setIcon(makeIcon(new Color(0x4A4A4A), 12));
    +255        intellij.getAccessibleContext().setAccessibleName("IntelliJ Darcula");
    +256        intellij.getAccessibleContext().setAccessibleDescription("Switch to the IntelliJ Darcula theme");
    +257        if ("darcula".equalsIgnoreCase(currentTheme)) {
    +258            intellij.setSelected(true);
    +259        }
    +260        themeGroup.add(intellij);
    +261        themesMenu.add(intellij);
    +262        themesMenu.addSeparator();
    +263
    +264        // Dynamically add all IntelliJ themes available from flatlaf-intellij-themes
    +265        // Discover and add IntelliJ themes from the flatlaf-intellij-themes artifact if present
    +266        List<String> intellijThemes = listClassesInPackage("com.formdev.flatlaf.intellijthemes");
    +267        if (!intellijThemes.isEmpty()) {
    +268            JMenu intellijGroup = new JMenu("IntelliJ Themes");
    +269            for (String cls : intellijThemes) {
    +270                final String className = cls;
    +271                    JMenuItem mi = new JMenuItem(new AbstractAction(simpleName(className)) {
    +272                    @Override public void actionPerformed(final ActionEvent e) { Main.setTheme(className); com.studentgui.apphelpers.Settings.put("theme", className); }
    +273                });
    +274                mi.setIcon(makeIcon(new Color(0x888888), 10));
    +275                mi.getAccessibleContext().setAccessibleName(className);
    +276                mi.getAccessibleContext().setAccessibleDescription("Apply " + className);
    +277                intellijGroup.add(mi);
    +278            }
    +279            themesMenu.add(intellijGroup);
    +280        }
    +281
    +282        // Material themes: if user adds flatlaf-themes or material themes library, we can try to load them by class name
    +283        JMenu materialGroup = new JMenu("Material Themes");
    +284        List<String> materialThemes = listClassesInPackage("com.formdev.flatlaf.materialthemes");
    +285        for (String cls : materialThemes) {
    +286            final String className = cls;
    +287            JMenuItem mi = new JMenuItem(new AbstractAction(simpleName(className)) {
    +288                @Override public void actionPerformed(final ActionEvent e) { Main.setTheme(className); com.studentgui.apphelpers.Settings.put("theme", className); }
    +289            });
    +290            mi.setIcon(makeIcon(new Color(0x666666), 10));
    +291            mi.getAccessibleContext().setAccessibleName(className);
    +292            mi.getAccessibleContext().setAccessibleDescription("Apply " + className);
    +293            materialGroup.add(mi);
    +294        }
    +295        themesMenu.add(materialGroup);
    +296
    +297        // Material Theme UI Lite (popular collection) - add specific known classes when available
    +298        JMenu materialLiteGroup = new JMenu("Material Theme UI Lite");
    +299        String[][] lite = new String[][]{
    +300            {"Arc Dark (Material)", "com.formdev.flatlaf.intellijthemes.materialthemeuilite.FlatArcDarkIJTheme"},
    +301            {"Arc Dark Contrast (Material)", "com.formdev.flatlaf.intellijthemes.materialthemeuilite.FlatArcDarkContrastIJTheme"},
    +302            {"Atom One Dark (Material)", "com.formdev.flatlaf.intellijthemes.materialthemeuilite.FlatAtomOneDarkIJTheme"},
    +303            {"Atom One Dark Contrast (Material)", "com.formdev.flatlaf.intellijthemes.materialthemeuilite.FlatAtomOneDarkContrastIJTheme"},
    +304            {"Atom One Light (Material)", "com.formdev.flatlaf.intellijthemes.materialthemeuilite.FlatAtomOneLightIJTheme"},
    +305            {"Atom One Light Contrast (Material)", "com.formdev.flatlaf.intellijthemes.materialthemeuilite.FlatAtomOneLightContrastIJTheme"},
    +306            {"Dracula (Material)", "com.formdev.flatlaf.intellijthemes.materialthemeuilite.FlatDraculaIJTheme"},
    +307            {"Dracula Contrast (Material)", "com.formdev.flatlaf.intellijthemes.materialthemeuilite.FlatDraculaContrastIJTheme"},
    +308            {"GitHub (Material)", "com.formdev.flatlaf.intellijthemes.materialthemeuilite.FlatGitHubIJTheme"},
    +309            {"GitHub Contrast (Material)", "com.formdev.flatlaf.intellijthemes.materialthemeuilite.FlatGitHubContrastIJTheme"},
    +310            {"GitHub Dark (Material)", "com.formdev.flatlaf.intellijthemes.materialthemeuilite.FlatGitHubDarkIJTheme"},
    +311            {"GitHub Dark Contrast (Material)", "com.formdev.flatlaf.intellijthemes.materialthemeuilite.FlatGitHubDarkContrastIJTheme"},
    +312            {"Light Owl (Material)", "com.formdev.flatlaf.intellijthemes.materialthemeuilite.FlatLightOwlIJTheme"},
    +313            {"Light Owl Contrast (Material)", "com.formdev.flatlaf.intellijthemes.materialthemeuilite.FlatLightOwlContrastIJTheme"},
    +314            {"Material Darker (Material)", "com.formdev.flatlaf.intellijthemes.materialthemeuilite.FlatMaterialDarkerIJTheme"},
    +315            {"Material Darker Contrast (Material)", "com.formdev.flatlaf.intellijthemes.materialthemeuilite.FlatMaterialDarkerContrastIJTheme"},
    +316            {"Material Deep Ocean (Material)", "com.formdev.flatlaf.intellijthemes.materialthemeuilite.FlatMaterialDeepOceanIJTheme"},
    +317            {"Material Deep Ocean Contrast (Material)", "com.formdev.flatlaf.intellijthemes.materialthemeuilite.FlatMaterialDeepOceanContrastIJTheme"},
    +318            {"Material Lighter (Material)", "com.formdev.flatlaf.intellijthemes.materialthemeuilite.FlatMaterialLighterIJTheme"},
    +319            {"Material Lighter Contrast (Material)", "com.formdev.flatlaf.intellijthemes.materialthemeuilite.FlatMaterialLighterContrastIJTheme"},
    +320            {"Material Oceanic (Material)", "com.formdev.flatlaf.intellijthemes.materialthemeuilite.FlatMaterialOceanicIJTheme"},
    +321            {"Material Oceanic Contrast (Material)", "com.formdev.flatlaf.intellijthemes.materialthemeuilite.FlatMaterialOceanicContrastIJTheme"},
    +322            {"Material Palenight (Material)", "com.formdev.flatlaf.intellijthemes.materialthemeuilite.FlatMaterialPalenightIJTheme"},
    +323            {"Material Palenight Contrast (Material)", "com.formdev.flatlaf.intellijthemes.materialthemeuilite.FlatMaterialPalenightContrastIJTheme"},
    +324            {"Monokai Pro (Material)", "com.formdev.flatlaf.intellijthemes.materialthemeuilite.FlatMonokaiProIJTheme"},
    +325            {"Monokai Pro Contrast (Material)", "com.formdev.flatlaf.intellijthemes.materialthemeuilite.FlatMonokaiProContrastIJTheme"},
    +326            {"Moonlight (Material)", "com.formdev.flatlaf.intellijthemes.materialthemeuilite.FlatMoonlightIJTheme"},
    +327            {"Moonlight Contrast (Material)", "com.formdev.flatlaf.intellijthemes.materialthemeuilite.FlatMoonlightContrastIJTheme"},
    +328            {"Night Owl (Material)", "com.formdev.flatlaf.intellijthemes.materialthemeuilite.FlatNightOwlIJTheme"},
    +329            {"Night Owl Contrast (Material)", "com.formdev.flatlaf.intellijthemes.materialthemeuilite.FlatNightOwlContrastIJTheme"},
    +330            {"Solarized Dark (Material)", "com.formdev.flatlaf.intellijthemes.materialthemeuilite.FlatSolarizedDarkIJTheme"},
    +331            {"Solarized Dark Contrast (Material)", "com.formdev.flatlaf.intellijthemes.materialthemeuilite.FlatSolarizedDarkContrastIJTheme"},
    +332            {"Solarized Light (Material)", "com.formdev.flatlaf.intellijthemes.materialthemeuilite.FlatSolarizedLightIJTheme"},
    +333            {"Solarized Light Contrast (Material)", "com.formdev.flatlaf.intellijthemes.materialthemeuilite.FlatSolarizedLightContrastIJTheme"}
    +334        };
    +335        for (String[] entry : lite) {
    +336            final String label = entry[0];
    +337            final String className = entry[1];
    +338            try {
    +339                Class.forName(className);
    +340                JMenuItem mi = new JMenuItem(new AbstractAction(label) {
    +341                    @Override public void actionPerformed(final ActionEvent e) { Main.setTheme(className); com.studentgui.apphelpers.Settings.put("theme", className); }
    +342                });
    +343                mi.setIcon(makeIcon(new Color(0x666666), 10));
    +344                mi.getAccessibleContext().setAccessibleName(className);
    +345                mi.getAccessibleContext().setAccessibleDescription("Apply " + className);
    +346                materialLiteGroup.add(mi);
    +347            } catch (ClassNotFoundException cnfe) {
    +348                // class not on classpath - ignore
    +349            }
    +350        }
    +351        // Only add the group if at least one theme was found
    +352        if (materialLiteGroup.getMenuComponentCount() > 0) {
    +353            themesMenu.add(materialLiteGroup);
    +354        }
    +355
    +356        mb.add(themesMenu);
    +357        return mb;
    +358    }
    +359
    +360    /**
    +361     * Private constructor to prevent instantiation of this utility class.
    +362     */
    +363    private Theme() {
    +364        throw new AssertionError("Not instantiable");
    +365    }
    +366
    +367    /**
    +368     * Create a small square color icon used for menu items. Kept local to avoid
    +369     * needing external resources; a simple filled rounded rectangle is drawn.
    +370     */
    +371    private static ImageIcon makeIcon(final Color color, final int size) {
    +372        BufferedImage img = new BufferedImage(size, size, BufferedImage.TYPE_INT_ARGB);
    +373        Graphics2D g = img.createGraphics();
    +374        try {
    +375            g.setRenderingHint(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON);
    +376            g.setColor(color);
    +377            g.fillRoundRect(0, 0, size, size, Math.max(2, size/4), Math.max(2, size/4));
    +378        } finally {
    +379            g.dispose();
    +380        }
    +381        return new ImageIcon(img);
    +382    }
    +383
    +384    // Return the simple class name from a fully-qualified class name
    +385    private static String simpleName(final String fqcn) {
    +386        int idx = fqcn.lastIndexOf('.');
    +387        return idx >= 0 ? fqcn.substring(idx + 1) : fqcn;
    +388    }
    +389
    +390    // List classes in a package by scanning classpath entries. This is a best-effort
    +391    // method: it handles classes inside jars and on the filesystem.
    +392    private static List<String> listClassesInPackage(final String packageName) {
    +393        List<String> results = new ArrayList<>();
    +394        String path = packageName.replace('.', '/');
    +395        try {
    +396            ClassLoader cl = Thread.currentThread().getContextClassLoader();
    +397            Enumeration<URL> resources = cl.getResources(path);
    +398            while (resources.hasMoreElements()) {
    +399                URL url = resources.nextElement();
    +400                URLConnection conn = url.openConnection();
    +401                if (conn instanceof JarURLConnection) {
    +402                    JarURLConnection juc = (JarURLConnection) conn;
    +403                    try (JarFile jar = juc.getJarFile()) {
    +404                        Enumeration<JarEntry> entries = jar.entries();
    +405                        while (entries.hasMoreElements()) {
    +406                            JarEntry je = entries.nextElement();
    +407                            String name = je.getName();
    +408                            if (name.startsWith(path) && name.endsWith(".class") && !je.isDirectory()) {
    +409                                String cls = name.replace('/', '.').replaceAll("\\.class$", "");
    +410                                results.add(cls);
    +411                            }
    +412                        }
    +413                    }
    +414                } else {
    +415                    try {
    +416                        URI uri = url.toURI();
    +417                        File folder = new File(uri);
    +418                        if (folder.isDirectory()) {
    +419                            File[] files = folder.listFiles();
    +420                            if (files != null) {
    +421                                for (File f : files) {
    +422                                    if (f.isFile() && f.getName().endsWith(".class")) {
    +423                                        String cls = packageName + "." + f.getName().replaceAll("\\.class$", "");
    +424                                        results.add(cls);
    +425                                    }
    +426                                }
    +427                            }
    +428                        }
    +429                    } catch (java.net.URISyntaxException ioe) {
    +430                        // ignore
    +431                    }
    +432                }
    +433            }
    +434        } catch (IOException e) {
    +435            // ignore
    +436        }
    +437        return results;
    +438    }
    +439}
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    + + diff --git a/target/site/apidocs/src-html/com/studentgui/bootstrap/Bootstrap.html b/target/site/apidocs/src-html/com/studentgui/bootstrap/Bootstrap.html new file mode 100644 index 0000000..ffc356f --- /dev/null +++ b/target/site/apidocs/src-html/com/studentgui/bootstrap/Bootstrap.html @@ -0,0 +1,120 @@ + + + + +Source code + + + + + + +
    +
    +
    001package com.studentgui.bootstrap;
    +002
    +003/**
    +004 * Lightweight bootstrapper that sets early system properties required by
    +005 * the logging subsystem (APP_HOME and LOG_TS) before delegating to the
    +006 * real application entry point. This ensures Logback picks up a stable
    +007 * per-run filename for the rolling file appender.
    +008 */
    +009public final class Bootstrap {
    +010    private Bootstrap() { throw new AssertionError("not instantiable"); }
    +011
    +012    public static void main(final String[] args) {
    +013        try {
    +014            String appHome = com.studentgui.apphelpers.Helpers.APP_HOME.toString();
    +015            System.setProperty("APP_HOME", appHome);
    +016        } catch (Throwable t) {
    +017            // Best-effort: if Helpers isn't available, fall back to a relative path
    +018            System.setProperty("APP_HOME", "app_home");
    +019        }
    +020        // Ensure a stable per-run timestamp for Logback file naming. Use
    +021        // the same yyyyMMddHHmmss pattern that logback's <timestamp>
    +022        // element uses so filenames match when possible.
    +023        try {
    +024            java.time.format.DateTimeFormatter df = java.time.format.DateTimeFormatter.ofPattern("yyyyMMddHHmmss").withZone(java.time.ZoneOffset.UTC);
    +025            String ts = df.format(java.time.Instant.now());
    +026            System.setProperty("LOG_TS", ts);
    +027        } catch (Exception ex) {
    +028            System.setProperty("LOG_TS", String.valueOf(java.time.Instant.now().getEpochSecond()));
    +029        }
    +030
    +031        // Create logs directory early to avoid races when Logback opens the file
    +032        try {
    +033            java.nio.file.Path logs = java.nio.file.Paths.get(System.getProperty("APP_HOME")).resolve("logs");
    +034            java.nio.file.Files.createDirectories(logs);
    +035        } catch (Exception ex) {
    +036            // ignore - best effort
    +037        }
    +038
    +039        // Delegate to the main application
    +040        com.studentgui.app.Main.main(args);
    +041    }
    +042}
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    + + diff --git a/target/site/apidocs/src-html/com/studentgui/test/BrailleSmokeTest.html b/target/site/apidocs/src-html/com/studentgui/test/BrailleSmokeTest.html new file mode 100644 index 0000000..3ddb173 --- /dev/null +++ b/target/site/apidocs/src-html/com/studentgui/test/BrailleSmokeTest.html @@ -0,0 +1,100 @@ + + + + +Source code + + + + + + +
    +
    +
    001package com.studentgui.test;
    +002
    +003/**
    +004 * Legacy smoke main retained for reference. Converted to a no-op deprecated
    +005 * holder to avoid duplicate Javadoc warnings now that an equivalent JUnit
    +006 * test exists under src/test/java.
    +007 *
    +008 * @deprecated Use {@code src/test/java/com/studentgui/test/BrailleSmokeTest.java}
    +009 *             (the JUnit 5 replacement) for automated smoke testing.
    +010 */
    +011@Deprecated
    +012public final class BrailleSmokeTest {
    +013    // intentionally empty - preserved for historical reference
    +014
    +015    /**
    +016     * Private constructor to prevent instantiation of this utility holder.
    +017     * The real smoke test has been converted to a JUnit test under src/test.
    +018     */
    +019    private BrailleSmokeTest() {
    +020        throw new AssertionError("Not instantiable");
    +021    }
    +022}
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    + + diff --git a/target/site/apidocs/src-html/com/studentgui/tools/GroupedSmoke.html b/target/site/apidocs/src-html/com/studentgui/tools/GroupedSmoke.html new file mode 100644 index 0000000..a970cfd --- /dev/null +++ b/target/site/apidocs/src-html/com/studentgui/tools/GroupedSmoke.html @@ -0,0 +1,175 @@ + + + + +Source code + + + + + + +
    +
    +
    001package com.studentgui.tools;
    +002
    +003import java.nio.file.Path;
    +004import java.time.LocalDate;
    +005import java.time.format.DateTimeFormatter;
    +006import java.util.ArrayList;
    +007import java.util.List;
    +008
    +009import com.studentgui.apphelpers.Helpers;
    +010import com.studentgui.apppages.JLineGraph;
    +011
    +012/**
    +013 * Automated smoke test for grouped chart rendering and multi-panel PNG export.
    +014 *
    +015 * <p>Verifies that {@link JLineGraph} correctly renders multiple stacked phase-grouped
    +016 * charts (as used by assessment pages like Braille, Abacus, etc.). Generates synthetic
    +017 * data with explicit phase prefixes (P1, P2, P3) and exports to PNG.</p>
    +018 *
    +019 * <p><b>Purpose:</b></p>
    +020 * <ul>
    +021 *   <li>Validates phase grouping logic in {@link JLineGraph#updateWithGroupedData}</li>
    +022 *   <li>Ensures each group renders as a separate stacked chart panel</li>
    +023 *   <li>Verifies PNG export of multi-chart layouts</li>
    +024 *   <li>Provides visual reference for chart appearance during development</li>
    +025 * </ul>
    +026 *
    +027 * <p><b>Usage:</b></p>
    +028 * <pre>{@code
    +029 * java -cp StudentDataGUI.jar com.studentgui.tools.GroupedSmoke
    +030 * }</pre>
    +031 *
    +032 * <p><b>Expected Output:</b></p>
    +033 * <pre>
    +034 * Grouped smoke wrote chart to: /path/to/app_home/StudentDataFiles/Grouped_Smoke/plots/GroupedSmoke-2024-01-15.png
    +035 * Exists: true
    +036 * </pre>
    +037 *
    +038 * <p><b>Test Data Structure:</b></p>
    +039 * <ul>
    +040 *   <li><b>Part codes:</b> 9 codes with prefixes: P1 (3 items), P2 (2 items), P3 (4 items)</li>
    +041 *   <li><b>Sessions:</b> 3 synthetic sessions with deterministic scores {@code (i + s) % 5}</li>
    +042 *   <li><b>Expected output:</b> 3 stacked chart panels (one per phase group) in a single 800×600px PNG</li>
    +043 * </ul>
    +044 *
    +045 * <p><b>Output Location:</b> {@code app_home/StudentDataFiles/Grouped_Smoke/plots/GroupedSmoke-<ISO_DATE>.png}</p>
    +046 *
    +047 * <p><b>Validation:</b> Inspect the generated PNG to verify:</p>
    +048 * <ol>
    +049 *   <li>Three distinct chart panels labeled "P1 - 3 items", "P2 - 2 items", "P3 - 4 items"</li>
    +050 *   <li>Each panel shows 3 line series (2 gray historical, 1 black latest)</li>
    +051 *   <li>Colored background bands visible in all panels</li>
    +052 * </ol>
    +053 *
    +054 * @see com.studentgui.apppages.JLineGraph#updateWithGroupedData
    +055 * @see com.studentgui.apppages.JLineGraph#saveChart
    +056 */
    +057public class GroupedSmoke {
    +058    /**
    +059     * Entry point for the grouped smoke utility.
    +060     *
    +061     * @param args ignored
    +062     * @throws Exception on unexpected IO or charting errors
    +063     */
    +064    public static void main(final String[] args) throws Exception {
    +065        Helpers.createFolderHierarchy();
    +066        JLineGraph graph = new JLineGraph();
    +067
    +068        // build part codes with P1_, P2_, P3_ groups (3+2+4 items)
    +069        String[] codes = new String[]{"P1_1","P1_2","P1_3","P2_1","P2_2","P3_1","P3_2","P3_3","P3_4"};
    +070
    +071        // Create sample data: 3 sessions
    +072        List<List<Integer>> data = new ArrayList<>();
    +073        for (int s = 0; s < 3; s++) {
    +074            List<Integer> row = new ArrayList<>();
    +075            for (int i = 0; i < codes.length; i++) {
    +076                row.add((i + s) % 5);
    +077            }
    +078            data.add(row);
    +079        }
    +080        graph.updateWithGroupedData(data, codes);
    +081
    +082        Path outDir = Helpers.APP_HOME.resolve("StudentDataFiles").resolve(Helpers.safeName("Grouped Smoke")).resolve("plots");
    +083        java.nio.file.Files.createDirectories(outDir);
    +084        DateTimeFormatter df = DateTimeFormatter.ISO_DATE;
    +085        Path outFile = outDir.resolve("GroupedSmoke-" + LocalDate.now().format(df) + ".png");
    +086        graph.saveChart(outFile, 800, 600);
    +087        System.out.println("Grouped smoke wrote chart to: " + outFile.toAbsolutePath());
    +088        System.out.println("Exists: " + java.nio.file.Files.exists(outFile));
    +089    }
    +090    /**
    +091     * Public no-arg constructor to document the utility nature of this class.
    +092     * Kept for completeness; all work is performed from {@link #main(String[])}.
    +093     */
    +094    public GroupedSmoke() {
    +095        // no state
    +096    }
    +097}
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    + + diff --git a/target/site/apidocs/src-html/com/studentgui/tools/ProgrammaticPageSaveTest.html b/target/site/apidocs/src-html/com/studentgui/tools/ProgrammaticPageSaveTest.html new file mode 100644 index 0000000..dd00a09 --- /dev/null +++ b/target/site/apidocs/src-html/com/studentgui/tools/ProgrammaticPageSaveTest.html @@ -0,0 +1,211 @@ + + + + +Source code + + + + + + +
    +
    +
    001package com.studentgui.tools;
    +002
    +003import java.time.LocalDate;
    +004import java.util.Arrays;
    +005import java.util.stream.IntStream;
    +006
    +007import javax.swing.JButton;
    +008
    +009import com.studentgui.apphelpers.Helpers;
    +010import com.studentgui.apppages.Braille;
    +011import com.studentgui.apppages.JLineGraph;
    +012
    +013/**
    +014 * Automated integration test for programmatic page manipulation and database submission.
    +015 *
    +016 * <p>Simulates user interaction with the {@link Braille} assessment page by:</p>
    +017 * <ol>
    +018 *   <li>Programmatically instantiating a Braille page with synthetic student/date</li>
    +019 *   <li>Using reflection to access and populate internal {@code PhaseScoreField} components</li>
    +020 *   <li>Locating the "Submit Braille Data" button via accessible name</li>
    +021 *   <li>Programmatically triggering the submit action via {@link JButton#doClick()}</li>
    +022 * </ol>
    +023 *
    +024 * <p><b>Purpose:</b></p>
    +025 * <ul>
    +026 *   <li>Validates end-to-end page submission workflow without GUI interaction</li>
    +027 *   <li>Tests database insert, JSON export, and PNG chart generation in automated context</li>
    +028 *   <li>Verifies reflection-based access to page internals for testing purposes</li>
    +029 *   <li>Provides reference for programmatic testing of other assessment pages</li>
    +030 * </ul>
    +031 *
    +032 * <p><b>Usage:</b></p>
    +033 * <pre>{@code
    +034 * java -cp StudentDataGUI.jar com.studentgui.tools.ProgrammaticPageSaveTest
    +035 * }</pre>
    +036 *
    +037 * <p><b>Expected Side Effects:</b></p>
    +038 * <ul>
    +039 *   <li>New Braille progress session inserted into database for student "Smoke Test"</li>
    +040 *   <li>JSON export written to {@code StudentDataFiles/Smoke_Test/Sessions/Braille/}</li>
    +041 *   <li>Phase-grouped PNG plots written to {@code StudentDataFiles/Smoke_Test/plots/}</li>
    +042 *   <li>Markdown and HTML reports generated in {@code StudentDataFiles/Smoke_Test/reports/}</li>
    +043 * </ul>
    +044 *
    +045 * <p><b>Reflection Usage:</b> Accesses private {@code skillFields} array in {@link Braille}
    +046 * to set all 64 Braille skills to a score of 3. This demonstrates how to programmatically
    +047 * manipulate page state for testing when public setters are not available.</p>
    +048 *
    +049 * <p><b>Validation:</b> After execution, inspect:</p>
    +050 * <ul>
    +051 *   <li>Database: {@code sqlite3 app_home/StudentDatabase/students.db "SELECT * FROM ProgressSession ORDER BY id DESC LIMIT 1;"}</li>
    +052 *   <li>JSON exports: {@code ls -lt app_home/StudentDataFiles/Smoke_Test/Sessions/Braille/}</li>
    +053 *   <li>Generated plots: {@code ls -lt app_home/StudentDataFiles/Smoke_Test/plots/}</li>
    +054 * </ul>
    +055 *
    +056 * <p><b>Note:</b> This test modifies the live database. Run in a test environment or
    +057 * use a separate APP_HOME directory to avoid polluting production data.</p>
    +058 *
    +059 * @see com.studentgui.apppages.Braille
    +060 * @see com.studentgui.uicomp.PhaseScoreField
    +061 * @see javax.swing.JButton#doClick()
    +062 */
    +063public class ProgrammaticPageSaveTest {
    +064    /**
    +065     * Program entry to run the programmatic page save test.
    +066     *
    +067     * @param args ignored
    +068     * @throws Exception on reflection or DB errors
    +069     */
    +070    public static void main(final String[] args) throws Exception {
    +071        Helpers.createFolderHierarchy();
    +072        JLineGraph graph = new JLineGraph();
    +073        Braille page = new Braille("Smoke Test", LocalDate.now(), graph);
    +074
    +075        // Set all fields to 3 via getComponents traversal
    +076        Arrays.stream(page.getComponents()).forEach(c -> {
    +077            // nothing here; we'll rely on the submit button to collect values from the internal PhaseScoreField instances
    +078        });
    +079
    +080        // Helper: find submit button by accessible name and click it
    +081        JButton submit = findButtonByAccessibleName(page, "Submit Braille Data");
    +082        if (submit == null) {
    +083            System.out.println("Submit button not found; aborting test");
    +084            return;
    +085        }
    +086
    +087        // Programmatically set values using the page's declared skillFields via reflection
    +088        try {
    +089            java.lang.reflect.Field f = Braille.class.getDeclaredField("skillFields");
    +090            f.setAccessible(true);
    +091            Object arr = f.get(page);
    +092            if (arr instanceof com.studentgui.uicomp.PhaseScoreField[]) {
    +093                com.studentgui.uicomp.PhaseScoreField[] s = (com.studentgui.uicomp.PhaseScoreField[]) arr;
    +094                IntStream.range(0, s.length).forEach(i -> s[i].setValue(3));
    +095            }
    +096        } catch (ReflectiveOperationException roe) {
    +097            roe.printStackTrace();
    +098            System.out.println("Unable to set skillFields via reflection");
    +099        }
    +100
    +101        // Trigger submit
    +102        System.out.println("Triggering submit button action...");
    +103        submit.doClick();
    +104
    +105        System.out.println("Programmatic submit triggered. Check app_home for outputs.");
    +106    }
    +107
    +108    private static JButton findButtonByAccessibleName(final java.awt.Container c, final String name) {
    +109        for (java.awt.Component comp : c.getComponents()) {
    +110            if (comp instanceof JButton) {
    +111                JButton b = (JButton) comp;
    +112                if (name.equals(b.getAccessibleContext().getAccessibleName())) {
    +113                    return b;
    +114                }
    +115            }
    +116            if (comp instanceof java.awt.Container) {
    +117                JButton r = findButtonByAccessibleName((java.awt.Container) comp, name);
    +118                if (r != null) {
    +119                    return r;
    +120                }
    +121            }
    +122        }
    +123        return null;
    +124    }
    +125
    +126    /**
    +127     * Private constructor to avoid instantiation - this class is a programmatic
    +128     * test harness containing only static helpers and a main method.
    +129     */
    +130    private ProgrammaticPageSaveTest() {
    +131        // no instances
    +132    }
    +133}
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    + + diff --git a/target/site/apidocs/src-html/com/studentgui/tools/QueryStudentData.html b/target/site/apidocs/src-html/com/studentgui/tools/QueryStudentData.html new file mode 100644 index 0000000..86a6317 --- /dev/null +++ b/target/site/apidocs/src-html/com/studentgui/tools/QueryStudentData.html @@ -0,0 +1,193 @@ + + + + +Source code + + + + + + +
    +
    +
    001package com.studentgui.tools;
    +002
    +003import java.sql.Connection;
    +004import java.sql.DriverManager;
    +005import java.sql.PreparedStatement;
    +006import java.sql.ResultSet;
    +007import java.util.List;
    +008
    +009import com.studentgui.apphelpers.Database;
    +010import com.studentgui.apphelpers.Helpers;
    +011
    +012/**
    +013 * Command-line inspection tool for viewing student database contents and schema statistics.
    +014 *
    +015 * <p>Provides a quick diagnostic view of database state without launching the GUI.
    +016 * Useful for:</p>
    +017 * <ul>
    +018 *   <li>Verifying student records exist in the database</li>
    +019 *   <li>Inspecting available progress types and their assessment part counts</li>
    +020 *   <li>Checking session data row sizes for debugging schema migrations</li>
    +021 *   <li>Quick manual data verification during development or troubleshooting</li>
    +022 * </ul>
    +023 *
    +024 * <p><b>Usage:</b></p>
    +025 * <pre>{@code
    +026 * # List all students and progress types with counts
    +027 * java -cp StudentDataGUI.jar com.studentgui.tools.QueryStudentData
    +028 *
    +029 * # Inspect specific student's progress types
    +030 * java -cp StudentDataGUI.jar com.studentgui.tools.QueryStudentData "Aaron A Aaronsson"
    +031 * }</pre>
    +032 *
    +033 * <p><b>Output Format:</b></p>
    +034 * <pre>
    +035 * Inspecting student: Aaron A Aaronsson
    +036 * ProgressType 'Braille' (id=1) parts=64 sessions=3
    +037 *  Sample row sizes: 64 values: [2, 3, 2, 3, 4, ...]
    +038 * ProgressType 'Abacus' (id=2) parts=22 sessions=1
    +039 *  Sample row sizes: 22 values: [0, 1, 2, 1, 3, ...]
    +040 * </pre>
    +041 *
    +042 * <p><b>Workflow:</b></p>
    +043 * <ol>
    +044 *   <li>Lists all known students via {@link Helpers#getStudents()}</li>
    +045 *   <li>Selects first student or uses command-line argument</li>
    +046 *   <li>Queries {@code ProgressType} table for all progress types</li>
    +047 *   <li>For each progress type: counts assessment parts and fetches sample session rows</li>
    +048 *   <li>Prints progress type name, ID, part count, session count, and sample row to stdout</li>
    +049 * </ol>
    +050 *
    +051 * @see com.studentgui.apphelpers.Database#fetchLatestAssessmentResults
    +052 * @see com.studentgui.apphelpers.Helpers#getStudents()
    +053 */
    +054public class QueryStudentData {
    +055    /**
    +056     * Command-line entry point. Prints progress types and a sample row for
    +057     * the specified or first-known student.
    +058     *
    +059     * @param args optional first argument is student display name
    +060     * @throws Exception on database errors
    +061     */
    +062    public static void main(final String[] args) throws Exception {
    +063        Helpers.createFolderHierarchy();
    +064        List<String> students = Helpers.getStudents();
    +065        String student = null;
    +066        if (args.length > 0) {
    +067            student = args[0];
    +068        }
    +069        if (student == null) {
    +070            System.out.println("Known students:");
    +071            for (String s : students) {
    +072                System.out.println(" - " + s);
    +073            }
    +074            if (!students.isEmpty()) {
    +075                student = students.get(0);
    +076            } else {
    +077                System.out.println("No students found in DB. Exiting.");
    +078                return;
    +079            }
    +080        }
    +081        System.out.println("Inspecting student: " + student);
    +082        // list progress types
    +083        try (Connection c = DriverManager.getConnection("jdbc:sqlite:" + Helpers.DATABASE_PATH.toString())) {
    +084            try (PreparedStatement ps = c.prepareStatement("SELECT id, name FROM ProgressType")) {
    +085                try (ResultSet rs = ps.executeQuery()) {
    +086                    while (rs.next()) {
    +087                        int ptId = rs.getInt("id");
    +088                        String ptName = rs.getString("name");
    +089                        // count parts
    +090                            int partCount = 0;
    +091                        try (PreparedStatement ps2 = c.prepareStatement("SELECT COUNT(*) FROM AssessmentPart WHERE progress_type_id = ?")) {
    +092                            ps2.setInt(1, ptId);
    +093                            try (ResultSet rs2 = ps2.executeQuery()) {
    +094                                if (rs2.next()) {
    +095                                    partCount = rs2.getInt(1);
    +096                                }
    +097                            }
    +098                        }
    +099                        List<List<Integer>> rows = Database.fetchLatestAssessmentResults(student, ptName, 5);
    +100                        System.out.println(String.format("ProgressType '%s' (id=%d) parts=%d sessions=%d", ptName, ptId, partCount, rows.size()));
    +101                        if (!rows.isEmpty()) {
    +102                            System.out.println(" Sample row sizes: " + rows.get(0).size() + " values: " + rows.get(0));
    +103                        }
    +104                    }
    +105                }
    +106            }
    +107        }
    +108    }
    +109    /**
    +110     * No-op public constructor to document this class as a small utility.
    +111     */
    +112    public QueryStudentData() {
    +113        // utility class; no state
    +114    }
    +115}
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    + + diff --git a/target/site/apidocs/src-html/com/studentgui/tools/RenderStudentProgress.html b/target/site/apidocs/src-html/com/studentgui/tools/RenderStudentProgress.html new file mode 100644 index 0000000..d82b52d --- /dev/null +++ b/target/site/apidocs/src-html/com/studentgui/tools/RenderStudentProgress.html @@ -0,0 +1,180 @@ + + + + +Source code + + + + + + +
    +
    +
    001package com.studentgui.tools;
    +002
    +003import java.nio.file.Path;
    +004import java.sql.Connection;
    +005import java.sql.DriverManager;
    +006import java.sql.PreparedStatement;
    +007import java.sql.ResultSet;
    +008import java.time.LocalDate;
    +009import java.time.format.DateTimeFormatter;
    +010import java.util.ArrayList;
    +011import java.util.List;
    +012
    +013import com.studentgui.apphelpers.Database;
    +014import com.studentgui.apphelpers.Helpers;
    +015import com.studentgui.apppages.JLineGraph;
    +016
    +017/**
    +018 * Command-line utility for offline student progress chart rendering and export.
    +019 *
    +020 * <p>This standalone tool generates PNG charts for a specific student and progress type
    +021 * without launching the full GUI application. Useful for:</p>
    +022 * <ul>
    +023 *   <li>Batch chart generation for multiple students/progress types</li>
    +024 *   <li>Debugging chart rendering issues outside the GUI context</li>
    +025 *   <li>Automated report generation in CI/CD pipelines</li>
    +026 *   <li>Creating historical chart snapshots for archival purposes</li>
    +027 * </ul>
    +028 *
    +029 * <p><b>Usage:</b></p>
    +030 * <pre>{@code
    +031 * java -cp StudentDataGUI.jar com.studentgui.tools.RenderStudentProgress "Aaron A Aaronsson" "Braille"
    +032 * }</pre>
    +033 *
    +034 * <p><b>Workflow:</b></p>
    +035 * <ol>
    +036 *   <li>Ensures app folder hierarchy exists via {@link Helpers#createFolderHierarchy()}</li>
    +037 *   <li>Queries database for canonical assessment part codes for the specified progress type</li>
    +038 *   <li>Fetches up to 5 most recent assessment sessions via {@link Database#fetchLatestAssessmentResults}</li>
    +039 *   <li>Renders grouped chart using {@link JLineGraph#updateWithGroupedData}</li>
    +040 *   <li>Exports PNG to {@code StudentDataFiles/<student>/plots/<ProgressType>-render-<date>.png}</li>
    +041 * </ol>
    +042 *
    +043 * <p><b>Output:</b> PNG file written to student's plots directory with filename format:
    +044 * {@code <ProgressType>-render-<ISO_DATE>.png}</p>
    +045 *
    +046 * @see com.studentgui.apphelpers.Database#fetchLatestAssessmentResults
    +047 * @see com.studentgui.apppages.JLineGraph
    +048 * @see com.studentgui.apphelpers.Helpers#createFolderHierarchy()
    +049 */
    +050public class RenderStudentProgress {
    +051    /**
    +052     * Render and write a progress chart for the provided student and progress type.
    +053     *
    +054     * @param args first arg: student display name, second arg: progress type name
    +055     * @throws Exception on I/O or database access errors
    +056     */
    +057    public static void main(final String[] args) throws Exception {
    +058        if (args.length < 2) {
    +059            System.out.println("Usage: RenderStudentProgress <Student Name> <ProgressTypeName>");
    +060            return;
    +061        }
    +062        String student = args[0];
    +063        String pt = args[1];
    +064        Helpers.createFolderHierarchy();
    +065        System.out.println("Rendering " + pt + " for " + student);
    +066
    +067        // fetch canonical part codes for progress type
    +068        List<String> codes = new ArrayList<>();
    +069        try (Connection c = DriverManager.getConnection("jdbc:sqlite:" + Helpers.DATABASE_PATH.toString())) {
    +070            try (PreparedStatement ps = c.prepareStatement("SELECT code FROM AssessmentPart ap JOIN ProgressType pt ON ap.progress_type_id = pt.id WHERE pt.name = ? ORDER BY ap.id ASC")) {
    +071                ps.setString(1, pt);
    +072                try (ResultSet rs = ps.executeQuery()) {
    +073                    while (rs.next()) codes.add(rs.getString(1));
    +074                }
    +075            }
    +076        }
    +077        if (codes.isEmpty()) {
    +078            System.out.println("No parts found for progress type: " + pt);
    +079            return;
    +080        }
    +081        String[] codeArr = codes.toArray(new String[0]);
    +082        List<List<Integer>> rows = Database.fetchLatestAssessmentResults(student, pt, 5);
    +083        if (rows == null || rows.isEmpty()) {
    +084            System.out.println("No session rows for student/progress: " + student + "/" + pt);
    +085            return;
    +086        }
    +087        JLineGraph g = new JLineGraph();
    +088        g.updateWithGroupedData(rows, codeArr);
    +089        Path out = Helpers.APP_HOME.resolve("StudentDataFiles").resolve(Helpers.safeName(student)).resolve("plots");
    +090        java.nio.file.Files.createDirectories(out);
    +091        DateTimeFormatter df = DateTimeFormatter.ISO_DATE;
    +092        Path file = out.resolve(pt + "-render-" + LocalDate.now().format(df) + ".png");
    +093        g.saveChart(file, 1000, 800);
    +094        System.out.println("Wrote: " + file.toAbsolutePath());
    +095    }
    +096    /**
    +097     * Explicit no-arg constructor with documentation to avoid default-constructor javadoc warnings.
    +098     */
    +099    public RenderStudentProgress() {
    +100        // utility
    +101    }
    +102}
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    + + diff --git a/target/site/apidocs/src-html/com/studentgui/tools/SmokeTest.html b/target/site/apidocs/src-html/com/studentgui/tools/SmokeTest.html new file mode 100644 index 0000000..db7c2b3 --- /dev/null +++ b/target/site/apidocs/src-html/com/studentgui/tools/SmokeTest.html @@ -0,0 +1,163 @@ + + + + +Source code + + + + + + +
    +
    +
    001package com.studentgui.tools;
    +002
    +003import java.nio.file.Path;
    +004import java.time.LocalDate;
    +005import java.time.format.DateTimeFormatter;
    +006import java.util.ArrayList;
    +007import java.util.List;
    +008
    +009import com.studentgui.apphelpers.Helpers;
    +010import com.studentgui.apppages.JLineGraph;
    +011
    +012/**
    +013 * Minimal automated smoke test for chart rendering and PNG export functionality.
    +014 *
    +015 * <p>Generates deterministic synthetic assessment data, renders it via {@link JLineGraph},
    +016 * and writes a PNG to the app data folder. Used to verify:</p>
    +017 * <ul>
    +018 *   <li>JFreeChart rendering pipeline functions correctly</li>
    +019 *   <li>PNG export via {@link JLineGraph#saveChart} produces valid image files</li>
    +020 *   <li>File I/O permissions and folder creation work as expected</li>
    +021 *   <li>Chart layout and visual appearance match expectations (manual review)</li>
    +022 * </ul>
    +023 *
    +024 * <p><b>Usage:</b></p>
    +025 * <pre>{@code
    +026 * java -cp StudentDataGUI.jar com.studentgui.tools.SmokeTest
    +027 * }</pre>
    +028 *
    +029 * <p><b>Expected Output:</b></p>
    +030 * <pre>
    +031 * Smoke test wrote chart to: /path/to/app_home/StudentDataFiles/Smoke_Test/plots/SmokeTest-2024-01-15.png
    +032 * Exists: true
    +033 * </pre>
    +034 *
    +035 * <p><b>Test Data:</b> Generates 3 synthetic sessions with 28 skills each, using
    +036 * the formula {@code (skillIndex + sessionIndex) % 5} to produce deterministic
    +037 * values in the 0–4 range.</p>
    +038 *
    +039 * <p><b>Output Location:</b> {@code app_home/StudentDataFiles/Smoke_Test/plots/SmokeTest-<ISO_DATE>.png}</p>
    +040 *
    +041 * <p><b>Validation:</b> Success is indicated by "Exists: true" output and a valid
    +042 * 800×400px PNG file at the reported path. Visual inspection of the chart should show
    +043 * 3 line series (2 gray, 1 black) with colored background bands.</p>
    +044 *
    +045 * @see com.studentgui.apppages.JLineGraph#updateWithData
    +046 * @see com.studentgui.apppages.JLineGraph#saveChart
    +047 * @see com.studentgui.apphelpers.Helpers#createFolderHierarchy()
    +048 */
    +049public class SmokeTest {
    +050    /**
    +051     * Entry point for the smoke test.
    +052     *
    +053     * @param args ignored
    +054     * @throws Exception on IO or chart errors
    +055     */
    +056    public static void main(final String[] args) throws Exception {
    +057        Helpers.createFolderHierarchy();
    +058        JLineGraph graph = new JLineGraph();
    +059
    +060        // Create sample data: 3 sessions, each with 28 skill values (0-4)
    +061        List<List<Integer>> data = new ArrayList<>();
    +062        for (int s = 0; s < 3; s++) {
    +063            List<Integer> row = new ArrayList<>();
    +064            for (int i = 0; i < 28; i++) {
    +065                row.add((i + s) % 5); // deterministic sample
    +066            }
    +067            data.add(row);
    +068        }
    +069        graph.updateWithData(data);
    +070
    +071        Path outDir = Helpers.APP_HOME.resolve("StudentDataFiles").resolve(Helpers.safeName("Smoke Test")).resolve("plots");
    +072        DateTimeFormatter df = DateTimeFormatter.ISO_DATE;
    +073        Path outFile = outDir.resolve("SmokeTest-" + LocalDate.now().format(df) + ".png");
    +074        graph.saveChart(outFile, 800, 400);
    +075        System.out.println("Smoke test wrote chart to: " + outFile.toAbsolutePath());
    +076        System.out.println("Exists: " + java.nio.file.Files.exists(outFile));
    +077    }
    +078
    +079    /**
    +080     * Private constructor to prevent instantiation of this utility test class.
    +081     */
    +082    private SmokeTest() {
    +083        // no instances
    +084    }
    +085}
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    + + diff --git a/target/site/apidocs/src-html/com/studentgui/uicomp/PhaseScoreField.html b/target/site/apidocs/src-html/com/studentgui/uicomp/PhaseScoreField.html new file mode 100644 index 0000000..ccca4bc --- /dev/null +++ b/target/site/apidocs/src-html/com/studentgui/uicomp/PhaseScoreField.html @@ -0,0 +1,349 @@ + + + + +Source code + + + + + + +
    +
    +
    001package com.studentgui.uicomp;
    +002
    +003import java.awt.Dimension;
    +004import java.awt.Font;
    +005import java.awt.GridBagConstraints;
    +006import java.awt.GridBagLayout;
    +007import java.awt.Insets;
    +008
    +009import javax.swing.BorderFactory;
    +010import javax.swing.JComponent;
    +011import javax.swing.JPanel;
    +012import javax.swing.JSpinner;
    +013import javax.swing.JTextArea;
    +014import javax.swing.SpinnerNumberModel;
    +015
    +016/**
    +017 * Reusable component that renders a wrapped descriptive label and a compact
    +018 * integer input (0..4). The label is a non-editable JTextArea that wraps at
    +019 * ~200px; the component adds a 20px left inset so the label appears offset.
    +020 * The spinner is aligned to the first line of the label.
    +021 */
    +022public class PhaseScoreField extends JPanel {
    +023    private static final long serialVersionUID = 1L;
    +024    private static final org.slf4j.Logger LOG = org.slf4j.LoggerFactory.getLogger(PhaseScoreField.class);
    +025    // Ensure we only log the font-adjustment debug once to avoid noisy output in headless tests.
    +026    private static final java.util.concurrent.atomic.AtomicBoolean FONT_ADJUST_LOGGED = new java.util.concurrent.atomic.AtomicBoolean(false);
    +027    /** Wrapped, read-only label area used to display the description text. */
    +028    private final JTextArea labelArea;
    +029    /** Numeric spinner used for 0..4 score entry. */
    +030    private final JSpinner spinner;
    +031    /** Container used to constrain the wrap width of the label area. */
    +032    private final JPanel labelWrap;
    +033    // Global label width (pixels) used to make all rows align; default ~200
    +034    // Note: intentionally non-final so pages can adjust it at runtime.
    +035    private static int globalLabelWidthPx = 200;
    +036
    +037    /** Horizontal spacer panel inserted to tune the gap between label and spinner. */
    +038    private final JPanel spacer;
    +039
    +040    /**
    +041     * Create a PhaseScoreField containing a wrapped label and a numeric spinner.
    +042     *
    +043     * @param labelText text label to display (may be multi-line)
    +044     * @param initial initial integer value for the spinner (0..4)
    +045     */
    +046    public PhaseScoreField(final String labelText, final int initial) {
    +047        super(new GridBagLayout());
    +048    this.labelArea = new JTextArea(labelText);
    +049    labelArea.setLineWrap(true);
    +050    labelArea.setWrapStyleWord(true);
    +051    labelArea.setEditable(false);
    +052    labelArea.setOpaque(false);
    +053    labelArea.setFocusable(false);
    +054    // Use explicit font so the appearance doesn't change when switching LAFs
    +055    Font labelFont = new Font(Font.SANS_SERIF, Font.PLAIN, 12);
    +056    labelArea.setFont(labelFont);
    +057    // Constrain width to the configured global label width so it doesn't expand.
    +058    // Pages set GLOBAL_LABEL_WIDTH_PX to (maxLabelPx + 50). We render the label
    +059    // area at (GLOBAL - 50) and insert a 50px spacer so the spinner sits
    +060    // exactly 50px after the longest label text.
    +061    int prefHeight = computePreferredHeight(labelFont, 2);
    +062    int labelWidth = Math.max(40, globalLabelWidthPx - 50);
    +063    java.awt.Dimension fixed = new java.awt.Dimension(labelWidth, prefHeight);
    +064    // Wrap the JTextArea in a small container to guarantee horizontal size
    +065    this.labelWrap = new JPanel(new java.awt.BorderLayout());
    +066    this.labelWrap.setPreferredSize(fixed);
    +067    this.labelWrap.setMinimumSize(fixed);
    +068    this.labelWrap.setMaximumSize(new java.awt.Dimension(labelWidth, Short.MAX_VALUE));
    +069    this.labelWrap.add(labelArea, java.awt.BorderLayout.CENTER);
    +070
    +071        this.spinner = new JSpinner(new SpinnerNumberModel(initial, 0, 4, 1));
    +072        JComponent editor = spinner.getEditor();
    +073        // Set explicit font for spinner editor to keep sizing consistent across themes
    +074        Font spinnerFont = new Font(Font.SANS_SERIF, Font.PLAIN, 12);
    +075        editor.setFont(spinnerFont);
    +076        // The editor is typically a JSpinner.DefaultEditor containing a JTextField
    +077        try {
    +078            java.lang.reflect.Field f = editor.getClass().getDeclaredField("textField");
    +079            f.setAccessible(true);
    +080            Object tf = f.get(editor);
    +081            if (tf instanceof javax.swing.JTextField tfField) {
    +082                tfField.setFont(spinnerFont);
    +083            }
    +084        } catch (ReflectiveOperationException roe) {
    +085            // Field may not exist on some LAF/editor implementations. Log this at most once
    +086            // to avoid excessive noise during automated test runs.
    +087            if (FONT_ADJUST_LOGGED.compareAndSet(false, true)) {
    +088                LOG.trace("Could not adjust spinner editor textField font (field missing or inaccessible)");
    +089            }
    +090        }
    +091        editor.setPreferredSize(new Dimension(48, 20));
    +092        spinner.setPreferredSize(new Dimension(48, 24));
    +093
    +094        setBorder(BorderFactory.createEmptyBorder(0, 20, 0, 0)); // left inset 20px
    +095
    +096    GridBagConstraints gbc = new GridBagConstraints();
    +097    // Label: fixed preferred width, do not expand horizontally
    +098    gbc.gridx = 0;
    +099    gbc.gridy = 0;
    +100    gbc.anchor = GridBagConstraints.NORTHWEST;
    +101    gbc.fill = GridBagConstraints.NONE; // keep label at preferred size
    +102    gbc.weightx = 0.0;
    +103    gbc.insets = new Insets(2, 2, 2, 8);
    +104    add(labelWrap, gbc);
    +105
    +106    // Spacer: compute width so the spinner ends up 50px after the rendered label text
    +107    gbc.gridx = 1;
    +108    gbc.gridy = 0;
    +109    gbc.anchor = GridBagConstraints.NORTHWEST;
    +110    gbc.fill = GridBagConstraints.NONE;
    +111    gbc.weightx = 0.0;
    +112    // Compute rendered text pixel width for this label (safe to call here)
    +113    int textPx = computeMaxLabelPixelWidth(labelFont, new String[] { labelText });
    +114    int paddingWithinWrap = Math.max(0, labelWidth - textPx);
    +115    int spacerWidth = Math.max(0, 50 - paddingWithinWrap);
    +116    this.spacer = new JPanel(); this.spacer.setPreferredSize(new java.awt.Dimension(spacerWidth, 1));
    +117    add(this.spacer, gbc);
    +118
    +119    // Spinner sits immediately to the right of the spacer
    +120    gbc.gridx = 2;
    +121    gbc.gridy = 0;
    +122    gbc.anchor = GridBagConstraints.NORTHWEST;
    +123    gbc.fill = GridBagConstraints.NONE;
    +124    gbc.weightx = 0.0;
    +125    add(spinner, gbc);
    +126
    +127    // Filler: consumes remaining horizontal space so the spinner doesn't get pushed to the far right
    +128    gbc.gridx = 3;
    +129    gbc.gridy = 0;
    +130    gbc.anchor = GridBagConstraints.NORTHWEST;
    +131    gbc.fill = GridBagConstraints.HORIZONTAL;
    +132    gbc.weightx = 1.0;
    +133    add(new JPanel(), gbc);
    +134
    +135    // After layout, adjust spacer so the visible gap between label and spinner is exactly 50px
    +136    javax.swing.SwingUtilities.invokeLater(() -> {
    +137        int labelRight = labelWrap.getX() + labelWrap.getWidth();
    +138        int actualGap = spinner.getX() - labelRight;
    +139        int desiredGap = 50;
    +140        int currentSpacer = this.spacer.getPreferredSize().width;
    +141        int delta = desiredGap - actualGap;
    +142        if (delta != 0) {
    +143            int newWidth = Math.max(0, currentSpacer + delta);
    +144            this.spacer.setPreferredSize(new java.awt.Dimension(newWidth, 1));
    +145            this.spacer.revalidate();
    +146            this.revalidate();
    +147            this.repaint();
    +148        }
    +149    });
    +150    }
    +151
    +152    /**
    +153     * Set a global label width used by all PhaseScoreField instances created
    +154     * after calling this method. This helps align the spinner input across
    +155     * multiple rows so the entry fields start at a consistent position.
    +156     *
    +157     * @param px target global label width in pixels (will be clamped to a sensible minimum)
    +158     */
    +159    public static void setGlobalLabelWidth(final int px) {
    +160        globalLabelWidthPx = Math.max(80, px);
    +161    }
    +162
    +163    private static int computePreferredHeight(final Font font, final int approxLines) {
    +164        if (font == null) {
    +165            return 40;
    +166        }
    +167        javax.swing.JLabel probe = new javax.swing.JLabel();
    +168        java.awt.FontMetrics fm = probe.getFontMetrics(font);
    +169        int h = fm.getHeight() * Math.max(1, approxLines) + 6;
    +170        return Math.max(40, h);
    +171    }
    +172
    +173    /**
    +174     * Return the configured global label width in pixels used by new instances.
    +175     *
    +176     * @return global label width in pixels
    +177     */
    +178    public static int getGlobalLabelWidth() { return globalLabelWidthPx; }
    +179
    +180    /**
    +181     * Compute the pixel width of the longest label string using the given
    +182     * font. Returns the maximum string width in pixels.
    +183    *
    +184    * @param font font to use when measuring (may be null to use default)
    +185    * @param labels array of label texts to measure
    +186    * @return maximum string width in pixels (>=0)
    +187     */
    +188    public static int computeMaxLabelPixelWidth(final java.awt.Font font, final String[] labels) {
    +189        if (labels == null || labels.length == 0) {
    +190            return globalLabelWidthPx;
    +191        }
    +192        javax.swing.JLabel probe = new javax.swing.JLabel();
    +193        java.awt.FontMetrics fm = probe.getFontMetrics(font != null ? font : probe.getFont());
    +194        int max = 0;
    +195        for (String s : labels) {
    +196            if (s != null) {
    +197                max = Math.max(max, fm.stringWidth(s));
    +198            }
    +199        }
    +200        return max;
    +201    }
    +202
    +203    /**
    +204     * Set the visible label text for this row.
    +205     *
    +206     * @param text new label text
    +207     */
    +208    public void setLabel(final String text) { labelArea.setText(text); }
    +209
    +210    /**
    +211     * Get the current label text for this field.
    +212     *
    +213     * @return label text
    +214     */
    +215    public String getLabel() { return labelArea.getText(); }
    +216
    +217    /**
    +218     * Get the integer value currently selected in the spinner.
    +219     *
    +220     * @return spinner integer value (0..4)
    +221     */
    +222    public int getValue() {
    +223        // If the user is mid-edit in the spinner's text field, try to commit the edit
    +224        try {
    +225            java.awt.Component ed = spinner.getEditor();
    +226            if (ed instanceof javax.swing.JSpinner.DefaultEditor editorComp) {
    +227                javax.swing.JFormattedTextField tf = editorComp.getTextField();
    +228                try { tf.commitEdit(); } catch (java.text.ParseException pe) { LOG.trace("Spinner editor parse error", pe); }
    +229            }
    +230    } catch (IllegalArgumentException | IllegalStateException re) { LOG.trace("Unexpected error committing spinner edit", re); }
    +231        return (Integer) spinner.getValue();
    +232    }
    +233
    +234    /**
    +235     * Set the spinner value clamped to the valid range (0..4).
    +236     *
    +237     * @param v desired spinner value
    +238     */
    +239    public void setValue(final int v) { spinner.setValue(Math.max(0, Math.min(4, v))); }
    +240
    +241    @Override
    +242    public void setName(final String name) {
    +243        super.setName(name);
    +244        spinner.setName(name);
    +245    }
    +246
    +247    // Diagnostics: expose spinner X and label wrap width (useful to verify layout)
    +248    /**
    +249     * Get the X coordinate of the spinner inside this component (pixels).
    +250     *
    +251     * @return spinner X position in pixels
    +252     */
    +253    public int getSpinnerX() { return spinner.getLocation().x; }
    +254
    +255    /**
    +256     * Return the configured label wrap container's current width in pixels.
    +257     *
    +258     * @return label wrap width in pixels
    +259     */
    +260    public int getLabelWrapWidth() { return labelWrap.getWidth(); }
    +261
    +262    /**
    +263     * Actual horizontal gap in pixels between the label wrap right edge and the spinner left edge.
    +264     *
    +265     * @return pixel gap between label and spinner
    +266     */
    +267    public int getActualGap() {
    +268        int labelRight = labelWrap.getX() + labelWrap.getWidth();
    +269        return spinner.getX() - labelRight;
    +270    }
    +271}
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    + + diff --git a/target/apidocs/stylesheet.css b/target/site/apidocs/stylesheet.css similarity index 100% rename from target/apidocs/stylesheet.css rename to target/site/apidocs/stylesheet.css diff --git a/target/apidocs/tag-search-index.js b/target/site/apidocs/tag-search-index.js similarity index 100% rename from target/apidocs/tag-search-index.js rename to target/site/apidocs/tag-search-index.js diff --git a/target/apidocs/type-search-index.js b/target/site/apidocs/type-search-index.js similarity index 100% rename from target/apidocs/type-search-index.js rename to target/site/apidocs/type-search-index.js diff --git a/target/site/checkstyle.html b/target/site/checkstyle.html new file mode 100644 index 0000000..f354a27 --- /dev/null +++ b/target/site/checkstyle.html @@ -0,0 +1,19470 @@ + + + + + + + + Vision Skills Progression Tracker – Checkstyle Results + + + + + + + + +
    + +
    +
    +
    +
    +

    Checkstyle Results

    +

    The following document contains the results of Checkstyle 9.3 with sun_checks.xml ruleset.

    +

    Summary

    + + + + + + + + + + +
    Files Info Warnings Errors
    49003096
    +

    Files

    + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
    File I W E
    Abacus.java002
    Braille.java001
    BrailleNote.java001
    BrailleSense.java001
    CVI.java001
    DigitalLiteracy.java001
    IOS.java001
    JLineGraph.java001
    Keyboarding.java001
    Main.java001
    ScreenReader.java001
    VersionUtil.java0011
    com/studentgui/app/Main.java0082
    com/studentgui/app/PreferencesDialog.java0010
    com/studentgui/apphelpers/Database.java0092
    com/studentgui/apphelpers/Helpers.java0050
    com/studentgui/apphelpers/PythonPlotter.java0016
    com/studentgui/apphelpers/SessionJsonWriter.java0025
    com/studentgui/apphelpers/Settings.java008
    com/studentgui/apphelpers/SqlGenerate.java0017
    com/studentgui/apphelpers/UiNotifier.java0019
    com/studentgui/apphelpers/dto/AssessmentPayload.java0010
    com/studentgui/apphelpers/dto/ContactPayload.java0015
    com/studentgui/apphelpers/dto/KeyboardingPayload.java0010
    com/studentgui/apphelpers/dto/NotesPayload.java006
    com/studentgui/apppages/Abacus.java00186
    com/studentgui/apppages/Braille.java00269
    com/studentgui/apppages/BrailleNote.java00259
    com/studentgui/apppages/BrailleSense.java00119
    com/studentgui/apppages/CVI.java00142
    com/studentgui/apppages/ContactLog.java00104
    com/studentgui/apppages/DigitalLiteracy.java00195
    com/studentgui/apppages/Homepage.java0019
    com/studentgui/apppages/IOS.java00216
    com/studentgui/apppages/InstructionalMaterials.java0053
    com/studentgui/apppages/JLineGraph.java00241
    com/studentgui/apppages/Keyboarding.java00160
    com/studentgui/apppages/Observations.java0074
    com/studentgui/apppages/ScreenReader.java00224
    com/studentgui/apppages/SessionNotes.java0074
    com/studentgui/apptheming/Theme.java00191
    com/studentgui/bootstrap/Bootstrap.java009
    com/studentgui/test/BrailleSmokeTest.java002
    com/studentgui/tools/GroupedSmoke.java0027
    com/studentgui/tools/ProgrammaticPageSaveTest.java0029
    com/studentgui/tools/QueryStudentData.java0015
    com/studentgui/tools/RenderStudentProgress.java0023
    com/studentgui/tools/SmokeTest.java0019
    com/studentgui/uicomp/PhaseScoreField.java0063
    +

    Rules

    + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
    CategoryRuleViolationsSeverity
    blocksEmptyBlock1 Error
    LeftCurly65 Error
    NeedBraces11 Error
    RightCurly3 Error
    codingHiddenField11 Error
    MagicNumber376 Error
    MultipleVariableDeclarations2 Error
    designDesignForExtension19 Error
    FinalClass11 Error
    HideUtilityClassConstructor3 Error
    VisibilityModifier21 Error
    javadocInvalidJavadocPosition11 Error
    JavadocMethod12 Error
    JavadocPackage10 Error
    JavadocStyle2 Error
    JavadocVariable38 Error
    MissingJavadocMethod1 Error
    miscFinalParameters35 Error
    NewlineAtEndOfFile1 Error
    namingConstantName4 Error
    regexpRegexpSingleline +
      +
    • format: "\s+$"
    • +
    • maximum: "0"
    • +
    • message: "Line has trailing spaces."
    • +
    • minimum: "0"
    12 Error
    sizesLineLength +
      +
    • fileExtensions: "java"
    1682 Error
    MethodLength1 Error
    ParameterNumber2 Error
    whitespaceNoWhitespaceAfter8 Error
    NoWhitespaceBefore1 Error
    OperatorWrap3 Error
    ParenPad2 Error
    WhitespaceAfter580 Error
    WhitespaceAround168 Error
    +

    Details

    +

    Abacus.java

    + + + + + + + + + + + + + + + + + + +
    SeverityCategoryRuleMessageLine
     ErrorjavadocJavadocPackageMissing package-info.java file.1
     ErrorsizesLineLengthLine is longer than 80 characters (found 91).2
    +

    Braille.java

    + + + + + + + + + + + + +
    SeverityCategoryRuleMessageLine
     ErrorsizesLineLengthLine is longer than 80 characters (found 91).2
    +

    BrailleNote.java

    + + + + + + + + + + + + +
    SeverityCategoryRuleMessageLine
     ErrorsizesLineLengthLine is longer than 80 characters (found 91).2
    +

    BrailleSense.java

    + + + + + + + + + + + + +
    SeverityCategoryRuleMessageLine
     ErrorsizesLineLengthLine is longer than 80 characters (found 91).2
    +

    CVI.java

    + + + + + + + + + + + + +
    SeverityCategoryRuleMessageLine
     ErrorsizesLineLengthLine is longer than 80 characters (found 91).2
    +

    DigitalLiteracy.java

    + + + + + + + + + + + + +
    SeverityCategoryRuleMessageLine
     ErrorsizesLineLengthLine is longer than 80 characters (found 91).2
    +

    IOS.java

    + + + + + + + + + + + + +
    SeverityCategoryRuleMessageLine
     ErrorsizesLineLengthLine is longer than 80 characters (found 91).2
    +

    JLineGraph.java

    + + + + + + + + + + + + +
    SeverityCategoryRuleMessageLine
     ErrormiscNewlineAtEndOfFileFile does not end with a newline.1
    +

    Keyboarding.java

    + + + + + + + + + + + + +
    SeverityCategoryRuleMessageLine
     ErrorsizesLineLengthLine is longer than 80 characters (found 91).2
    +

    Main.java

    + + + + + + + + + + + + +
    SeverityCategoryRuleMessageLine
     ErrorsizesLineLengthLine is longer than 80 characters (found 84).1
    +

    ScreenReader.java

    + + + + + + + + + + + + +
    SeverityCategoryRuleMessageLine
     ErrorsizesLineLengthLine is longer than 80 characters (found 91).2
    +

    VersionUtil.java

    + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
    SeverityCategoryRuleMessageLine
     ErrordesignFinalClassClass VersionUtil should be declared as final.15
     ErrorsizesLineLengthLine is longer than 80 characters (found 81).16
     ErrorjavadocJavadocVariableMissing a Javadoc comment.16
     ErrorjavadocInvalidJavadocPositionJavadoc comment is placed in the wrong location.24
     ErrorsizesLineLengthLine is longer than 80 characters (found 83).27
     ErrorsizesLineLengthLine is longer than 80 characters (found 91).28
     ErrorsizesLineLengthLine is longer than 80 characters (found 87).32
     ErrorsizesLineLengthLine is longer than 80 characters (found 84).35
     ErrorsizesLineLengthLine is longer than 80 characters (found 85).44
     ErrorsizesLineLengthLine is longer than 80 characters (found 103).53
     ErrorsizesLineLengthLine is longer than 80 characters (found 92).54
    +

    com/studentgui/app/Main.java

    + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
    SeverityCategoryRuleMessageLine
     ErrorjavadocJavadocPackageMissing package-info.java file.1
     ErrorjavadocInvalidJavadocPositionJavadoc comment is placed in the wrong location.42
     ErrorjavadocInvalidJavadocPositionJavadoc comment is placed in the wrong location.50
     ErrorjavadocInvalidJavadocPositionJavadoc comment is placed in the wrong location.55
     ErrordesignFinalClassClass Main should be declared as final.65
     ErrorjavadocInvalidJavadocPositionJavadoc comment is placed in the wrong location.66
     ErrorsizesLineLengthLine is longer than 80 characters (found 83).75
     ErrorsizesLineLengthLine is longer than 80 characters (found 81).78
     ErrorsizesLineLengthLine is longer than 80 characters (found 87).82
     ErrorsizesLineLengthLine is longer than 80 characters (found 82).85
     ErrorsizesLineLengthLine is longer than 80 characters (found 101).86
     ErrorcodingMagicNumber'7' is a magic number.86
     ErrorsizesLineLengthLine is longer than 80 characters (found 132).87
     ErrorsizesLineLengthLine is longer than 80 characters (found 105).90
     ErrorsizesLineLengthLine is longer than 80 characters (found 83).95
     ErrorsizesLineLengthLine is longer than 80 characters (found 140).102
     ErrorsizesLineLengthLine is longer than 80 characters (found 105).105
     ErrorsizesLineLengthLine is longer than 80 characters (found 83).110
     ErrorsizesLineLengthLine is longer than 80 characters (found 84).117
     ErrorjavadocJavadocVariableMissing a Javadoc comment.121
     ErrorjavadocJavadocVariableMissing a Javadoc comment.122
     ErrorjavadocJavadocVariableMissing a Javadoc comment.123
     ErrorjavadocJavadocVariableMissing a Javadoc comment.124
     ErrorsizesLineLengthLine is longer than 80 characters (found 81).137
     ErrorjavadocJavadocVariableMissing a Javadoc comment.139
     ErrorsizesLineLengthLine is longer than 80 characters (found 126).141
     ErrorjavadocJavadocVariableMissing a Javadoc comment.141
     ErrornamingConstantNameName 'dateListeners' must match pattern '^[A-Z][A-Z0-9]*(_[A-Z0-9]+)*$'.141
     ErrorsizesLineLengthLine is longer than 80 characters (found 95).144
     ErrorregexpRegexpSinglelineLine has trailing spaces.148
     ErrorregexpRegexpSinglelineLine has trailing spaces.159
     ErrorregexpRegexpSinglelineLine has trailing spaces.168
     ErrorsizesLineLengthLine is longer than 80 characters (found 82).173
     ErrorsizesLineLengthLine is longer than 80 characters (found 132).187
     ErrorjavadocJavadocVariableMissing a Javadoc comment.187
     ErrornamingConstantNameName 'studentListeners' must match pattern '^[A-Z][A-Z0-9]*(_[A-Z0-9]+)*$'.187
     ErrorsizesLineLengthLine is longer than 80 characters (found 83).204
     ErrorsizesLineLengthLine is longer than 80 characters (found 88).218
     ErrorsizesLineLengthLine is longer than 80 characters (found 134).235
     ErrorjavadocJavadocVariableMissing a Javadoc comment.235
     ErrornamingConstantNameName 'settingsListeners' must match pattern '^[A-Z][A-Z0-9]*(_[A-Z0-9]+)*$'.235
     ErrorsizesLineLengthLine is longer than 80 characters (found 89).239
     ErrorsizesLineLengthLine is longer than 80 characters (found 82).244
     ErrorsizesLineLengthLine is longer than 80 characters (found 85).255
     ErrorsizesLineLengthLine is longer than 80 characters (found 92).269
     ErrorcodingMagicNumber'1000' is a magic number.304
     ErrorcodingMagicNumber'700' is a magic number.304
     ErrorsizesLineLengthLine is longer than 80 characters (found 101).307
     ErrorsizesLineLengthLine is longer than 80 characters (found 81).324
     ErrorsizesLineLengthLine is longer than 80 characters (found 88).328
     ErrorblocksLeftCurly'{' at column 64 should have line break after.328
     ErrorsizesLineLengthLine is longer than 80 characters (found 87).350
     ErrorsizesLineLengthLine is longer than 80 characters (found 89).360
     ErrorsizesLineLengthLine is longer than 80 characters (found 84).361
     ErrorsizesLineLengthLine is longer than 80 characters (found 86).363
     ErrorsizesLineLengthLine is longer than 80 characters (found 84).372
     ErrorsizesLineLengthLine is longer than 80 characters (found 84).376
     ErrorsizesLineLengthLine is longer than 80 characters (found 85).380
     ErrorsizesLineLengthLine is longer than 80 characters (found 101).383
     ErrorsizesLineLengthLine is longer than 80 characters (found 113).387
     ErrorsizesLineLengthLine is longer than 80 characters (found 84).391
     ErrorsizesLineLengthLine is longer than 80 characters (found 90).393
     ErrorsizesLineLengthLine is longer than 80 characters (found 101).396
     ErrorsizesLineLengthLine is longer than 80 characters (found 118).398
     ErrorsizesLineLengthLine is longer than 80 characters (found 98).399
     ErrorsizesLineLengthLine is longer than 80 characters (found 97).400
     ErrorsizesLineLengthLine is longer than 80 characters (found 101).406
     ErrorsizesLineLengthLine is longer than 80 characters (found 127).407
     ErrorsizesLineLengthLine is longer than 80 characters (found 93).415
     ErrorsizesLineLengthLine is longer than 80 characters (found 123).424
     ErrorsizesLineLengthLine is longer than 80 characters (found 88).432
     ErrorcodingMagicNumber'10' is a magic number.436
     ErrorsizesLineLengthLine is longer than 80 characters (found 119).454
     ErrorsizesLineLengthLine is longer than 80 characters (found 99).466
     ErrorjavadocInvalidJavadocPositionJavadoc comment is placed in the wrong location.471
     ErrorsizesLineLengthLine is longer than 80 characters (found 83).482
     ErrorsizesLineLengthLine is longer than 80 characters (found 106).483
     ErrorsizesLineLengthLine is longer than 80 characters (found 101).497
     ErrorsizesLineLengthLine is longer than 80 characters (found 84).534
     ErrorsizesLineLengthLine is longer than 80 characters (found 87).535
     ErrorsizesLineLengthLine is longer than 80 characters (found 83).567
     ErrorsizesLineLengthLine is longer than 80 characters (found 81).569
    +

    com/studentgui/app/PreferencesDialog.java

    + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
    SeverityCategoryRuleMessageLine
     ErrorblocksLeftCurly'{' at column 33 should have line break after.23
     ErrorsizesLineLengthLine is longer than 80 characters (found 93).37
     ErrorsizesLineLengthLine is longer than 80 characters (found 100).38
     ErrorsizesLineLengthLine is longer than 80 characters (found 87).40
     ErrorsizesLineLengthLine is longer than 80 characters (found 81).42
     ErrorsizesLineLengthLine is longer than 80 characters (found 87).43
     ErrorsizesLineLengthLine is longer than 80 characters (found 82).44
     ErrorsizesLineLengthLine is longer than 80 characters (found 88).45
     ErrorsizesLineLengthLine is longer than 80 characters (found 82).58
     ErrorsizesLineLengthLine is longer than 80 characters (found 85).59
    +

    com/studentgui/apphelpers/Database.java

    + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
    SeverityCategoryRuleMessageLine
     ErrordesignFinalClassClass Database should be declared as final.23
     ErrorsizesLineLengthLine is longer than 80 characters (found 92).34
     ErrorregexpRegexpSinglelineLine has trailing spaces.43
     ErrorsizesLineLengthLine is longer than 80 characters (found 81).51
     ErrorsizesLineLengthLine is longer than 80 characters (found 102).53
     ErrorsizesLineLengthLine is longer than 80 characters (found 134).61
     ErrorsizesLineLengthLine is longer than 80 characters (found 86).81
     ErrorsizesLineLengthLine is longer than 80 characters (found 107).83
     ErrorsizesLineLengthLine is longer than 80 characters (found 139).91
     ErrorsizesLineLengthLine is longer than 80 characters (found 114).112
     ErrorsizesLineLengthLine is longer than 80 characters (found 158).114
     ErrorsizesLineLengthLine is longer than 80 characters (found 122).134
     ErrorblocksLeftCurly'{' at column 28 should have line break after.141
     ErrorsizesLineLengthLine is longer than 80 characters (found 121).144
     ErrorsizesLineLengthLine is longer than 80 characters (found 82).156
     ErrorsizesLineLengthLine is longer than 80 characters (found 134).164
     ErrorsizesLineLengthLine is longer than 80 characters (found 191).166
     ErrorcodingMagicNumber'3' is a magic number.169
     ErrorblocksLeftCurly'{' at column 38 should have line break after.172
     ErrorsizesLineLengthLine is longer than 80 characters (found 84).180
     ErrorsizesLineLengthLine is longer than 80 characters (found 157).190
     ErrorsizesLineLengthLine is longer than 80 characters (found 118).191
     ErrorblocksLeftCurly'{' at column 44 should have line break after.191
     ErrorsizesLineLengthLine is longer than 80 characters (found 127).195
     ErrorsizesLineLengthLine is longer than 80 characters (found 140).203
     ErrorwhitespaceParenPad')' is preceded with whitespace.203
     ErrorcodingMagicNumber'3' is a magic number.212
     ErrorsizesLineLengthLine is longer than 80 characters (found 85).221
     ErrorsizesLineLengthLine is longer than 80 characters (found 162).231
     ErrorsizesLineLengthLine is longer than 80 characters (found 102).235
     ErrorblocksLeftCurly'{' at column 36 should have line break after.238
     ErrorblocksLeftCurly'{' at column 36 should have line break after.241
     ErrorsizesLineLengthLine is longer than 80 characters (found 107).244
     ErrorblocksLeftCurly'{' at column 36 should have line break after.247
     ErrorblocksLeftCurly'{' at column 41 should have line break after.250
     ErrorsizesLineLengthLine is longer than 80 characters (found 143).254
     ErrorblocksLeftCurly'{' at column 39 should have line break after.257
     ErrorsizesLineLengthLine is longer than 80 characters (found 166).263
     ErrorcodingMagicNumber'3' is a magic number.266
     ErrorblocksLeftCurly'{' at column 39 should have line break after.268
     ErrorsizesLineLengthLine is longer than 80 characters (found 133).275
     ErrorsizesLineLengthLine is longer than 80 characters (found 86).279
     ErrorsizesLineLengthLine is longer than 80 characters (found 85).303
     ErrordesignVisibilityModifierVariable 'dates' must be private and have accessor methods.305
     ErrordesignVisibilityModifierVariable 'rows' must be private and have accessor methods.312
     ErrorsizesLineLengthLine is longer than 80 characters (found 122).320
     ErrormiscFinalParametersParameter dates should be final.320
     ErrorcodingHiddenField'dates' hides a field.320
     ErrormiscFinalParametersParameter rows should be final.320
     ErrorcodingHiddenField'rows' hides a field.320
     ErrorsizesLineLengthLine is longer than 80 characters (found 82).328
     ErrorsizesLineLengthLine is longer than 80 characters (found 100).333
     ErrorsizesLineLengthLine is longer than 80 characters (found 168).336
     ErrorsizesLineLengthLine is longer than 80 characters (found 102).341
     ErrorsizesLineLengthLine is longer than 80 characters (found 82).349
     ErrorblocksLeftCurly'{' at column 36 should have line break after.349
     ErrorsizesLineLengthLine is longer than 80 characters (found 107).352
     ErrorsizesLineLengthLine is longer than 80 characters (found 143).366
     ErrorsizesLineLengthLine is longer than 80 characters (found 82).375
     ErrorsizesLineLengthLine is longer than 80 characters (found 81).377
     ErrorsizesLineLengthLine is longer than 80 characters (found 172).378
     ErrorsizesLineLengthLine is longer than 80 characters (found 91).379
     ErrorcodingMagicNumber'3' is a magic number.379
     ErrorsizesLineLengthLine is longer than 80 characters (found 90).383
     ErrorsizesLineLengthLine is longer than 80 characters (found 133).395
     ErrorsizesLineLengthLine is longer than 80 characters (found 86).399
     ErrorsizesLineLengthLine is longer than 80 characters (found 168).425
     ErrorsizesLineLengthLine is longer than 80 characters (found 162).427
     ErrorcodingMagicNumber'3' is a magic number.430
     ErrorcodingMagicNumber'4' is a magic number.431
     ErrorcodingMagicNumber'5' is a magic number.432
     ErrorsizesLineLengthLine is longer than 80 characters (found 102).445
     ErrorsizesLineLengthLine is longer than 80 characters (found 114).447
     ErrorsizesLineLengthLine is longer than 80 characters (found 349).472
     ErrorsizesParameterNumberMore than 7 parameters (found 11).472
     ErrorsizesLineLengthLine is longer than 80 characters (found 300).474
     ErrorwhitespaceParenPad')' is preceded with whitespace.474
     ErrorcodingMagicNumber'3' is a magic number.477
     ErrorcodingMagicNumber'4' is a magic number.478
     ErrorcodingMagicNumber'5' is a magic number.479
     ErrorcodingMagicNumber'6' is a magic number.480
     ErrorcodingMagicNumber'7' is a magic number.481
     ErrorcodingMagicNumber'8' is a magic number.482
     ErrorcodingMagicNumber'9' is a magic number.483
     ErrorcodingMagicNumber'10' is a magic number.484
     ErrorcodingMagicNumber'11' is a magic number.485
     ErrorsizesLineLengthLine is longer than 80 characters (found 132).499
     ErrorsizesLineLengthLine is longer than 80 characters (found 102).502
     ErrorsizesLineLengthLine is longer than 80 characters (found 107).516
     ErrorsizesLineLengthLine is longer than 80 characters (found 166).529
     ErrorsizesLineLengthLine is longer than 80 characters (found 280).542
     ErrorsizesLineLengthLine is longer than 80 characters (found 122).546
    +

    com/studentgui/apphelpers/Helpers.java

    + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
    SeverityCategoryRuleMessageLine
     ErrorjavadocJavadocPackageMissing package-info.java file.1
     ErrordesignFinalClassClass Helpers should be declared as final.17
     ErrorsizesLineLengthLine is longer than 80 characters (found 97).24
     ErrorjavadocJavadocVariableMissing a Javadoc comment.24
     ErrorsizesLineLengthLine is longer than 80 characters (found 86).26
     ErrorsizesLineLengthLine is longer than 80 characters (found 89).27
     ErrorsizesLineLengthLine is longer than 80 characters (found 82).32
     ErrorsizesLineLengthLine is longer than 80 characters (found 90).34
     ErrorjavadocJavadocMethod@return tag should be present and have description.41
     ErrorsizesLineLengthLine is longer than 80 characters (found 84).51
     ErrorsizesLineLengthLine is longer than 80 characters (found 97).53
     ErrorsizesLineLengthLine is longer than 80 characters (found 83).57
     ErrorjavadocInvalidJavadocPositionJavadoc comment is placed in the wrong location.68
     ErrorjavadocInvalidJavadocPositionJavadoc comment is placed in the wrong location.84
     ErrorsizesLineLengthLine is longer than 80 characters (found 93).91
     ErrorjavadocInvalidJavadocPositionJavadoc comment is placed in the wrong location.101
     ErrorsizesLineLengthLine is longer than 80 characters (found 92).116
     ErrorsizesLineLengthLine is longer than 80 characters (found 84).123
     ErrorsizesLineLengthLine is longer than 80 characters (found 94).124
     ErrorsizesLineLengthLine is longer than 80 characters (found 91).125
     ErrorsizesLineLengthLine is longer than 80 characters (found 99).131
     ErrorjavadocJavadocMethod@return tag should be present and have description.140
     ErrorjavadocJavadocMethodExpected @param tag for 's'.140
     ErrorwhitespaceWhitespaceAfter',' is not followed by whitespace.148
     ErrorwhitespaceWhitespaceAfter',' is not followed by whitespace.148
     ErrorwhitespaceWhitespaceAfter',' is not followed by whitespace.148
     ErrorwhitespaceWhitespaceAfter',' is not followed by whitespace.148
     ErrorwhitespaceWhitespaceAfter',' is not followed by whitespace.148
     ErrorwhitespaceWhitespaceAfter',' is not followed by whitespace.148
     ErrorwhitespaceWhitespaceAfter',' is not followed by whitespace.148
     ErrorwhitespaceWhitespaceAfter',' is not followed by whitespace.148
     ErrorwhitespaceWhitespaceAfter',' is not followed by whitespace.148
     ErrorsizesLineLengthLine is longer than 80 characters (found 100).183
     ErrorsizesLineLengthLine is longer than 80 characters (found 133).192
     ErrorsizesLineLengthLine is longer than 80 characters (found 105).198
     ErrorsizesLineLengthLine is longer than 80 characters (found 110).199
     ErrorsizesLineLengthLine is longer than 80 characters (found 96).215
     ErrorsizesLineLengthLine is longer than 80 characters (found 100).221
     ErrorsizesLineLengthLine is longer than 80 characters (found 100).225
     ErrorsizesLineLengthLine is longer than 80 characters (found 82).230
     ErrorsizesLineLengthLine is longer than 80 characters (found 102).231
     ErrorsizesLineLengthLine is longer than 80 characters (found 114).235
     ErrorsizesLineLengthLine is longer than 80 characters (found 88).240
     ErrorsizesLineLengthLine is longer than 80 characters (found 109).241
     ErrorsizesLineLengthLine is longer than 80 characters (found 94).245
     ErrorsizesLineLengthLine is longer than 80 characters (found 94).246
     ErrorsizesLineLengthLine is longer than 80 characters (found 90).251
     ErrorsizesLineLengthLine is longer than 80 characters (found 98).260
     ErrorsizesLineLengthLine is longer than 80 characters (found 94).261
     ErrorjavadocInvalidJavadocPositionJavadoc comment is placed in the wrong location.288
    +

    com/studentgui/apphelpers/PythonPlotter.java

    + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
    SeverityCategoryRuleMessageLine
     ErrordesignFinalClassClass PythonPlotter should be declared as final.19
     ErrorsizesLineLengthLine is longer than 80 characters (found 83).20
     ErrorjavadocJavadocVariableMissing a Javadoc comment.20
     ErrorsizesLineLengthLine is longer than 80 characters (found 90).23
     ErrorsizesLineLengthLine is longer than 80 characters (found 84).24
     ErrorsizesLineLengthLine is longer than 80 characters (found 81).27
     ErrorsizesLineLengthLine is longer than 80 characters (found 94).29
     ErrorsizesLineLengthLine is longer than 80 characters (found 123).31
     ErrorsizesLineLengthLine is longer than 80 characters (found 86).41
     ErrorsizesLineLengthLine is longer than 80 characters (found 109).46
     ErrorsizesLineLengthLine is longer than 80 characters (found 104).50
     ErrorsizesLineLengthLine is longer than 80 characters (found 84).57
     ErrorsizesLineLengthLine is longer than 80 characters (found 90).60
     ErrorsizesLineLengthLine is longer than 80 characters (found 85).69
     ErrorsizesLineLengthLine is longer than 80 characters (found 86).70
     ErrorsizesLineLengthLine is longer than 80 characters (found 119).71
    +

    com/studentgui/apphelpers/SessionJsonWriter.java

    + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
    SeverityCategoryRuleMessageLine
     ErrorsizesLineLengthLine is longer than 80 characters (found 87).21
     ErrorjavadocJavadocVariableMissing a Javadoc comment.21
     ErrorjavadocJavadocVariableMissing a Javadoc comment.22
     ErrorwhitespaceWhitespaceAround'{' is not followed by whitespace.24
     ErrorwhitespaceWhitespaceAround'}' is not preceded with whitespace.24
     ErrorsizesLineLengthLine is longer than 80 characters (found 83).28
     ErrorsizesLineLengthLine is longer than 80 characters (found 108).35
     ErrorsizesLineLengthLine is longer than 80 characters (found 82).40
     ErrorsizesLineLengthLine is longer than 80 characters (found 81).41
     ErrorsizesLineLengthLine is longer than 80 characters (found 81).42
     ErrorsizesLineLengthLine is longer than 80 characters (found 85).45
     ErrorsizesLineLengthLine is longer than 80 characters (found 87).50
     ErrorsizesLineLengthLine is longer than 80 characters (found 140).53
     ErrorsizesLineLengthLine is longer than 80 characters (found 146).62
     ErrorsizesLineLengthLine is longer than 80 characters (found 113).64
     ErrorsizesLineLengthLine is longer than 80 characters (found 97).66
     ErrorsizesLineLengthLine is longer than 80 characters (found 96).67
     ErrorsizesLineLengthLine is longer than 80 characters (found 127).73
     ErrorsizesLineLengthLine is longer than 80 characters (found 95).86
     ErrorsizesLineLengthLine is longer than 80 characters (found 92).88
     ErrorsizesLineLengthLine is longer than 80 characters (found 106).91
     ErrorsizesLineLengthLine is longer than 80 characters (found 137).106
     ErrorsizesLineLengthLine is longer than 80 characters (found 97).107
     ErrorsizesLineLengthLine is longer than 80 characters (found 128).121
     ErrorsizesLineLengthLine is longer than 80 characters (found 136).122
    +

    com/studentgui/apphelpers/Settings.java

    + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
    SeverityCategoryRuleMessageLine
     ErrorsizesLineLengthLine is longer than 80 characters (found 89).14
     ErrorjavadocJavadocVariableMissing a Javadoc comment.14
     ErrorjavadocJavadocVariableMissing a Javadoc comment.15
     ErrornamingConstantNameName 'props' must match pattern '^[A-Z][A-Z0-9]*(_[A-Z0-9]+)*$'.15
     ErrorsizesLineLengthLine is longer than 80 characters (found 98).16
     ErrorjavadocJavadocVariableMissing a Javadoc comment.16
     ErrorsizesLineLengthLine is longer than 80 characters (found 105).20
     ErrorblocksLeftCurly'{' at column 24 should have line break after.29
    +

    com/studentgui/apphelpers/SqlGenerate.java

    + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
    SeverityCategoryRuleMessageLine
     ErrorjavadocInvalidJavadocPositionJavadoc comment is placed in the wrong location.14
     ErrordesignFinalClassClass SqlGenerate should be declared as final.25
     ErrorjavadocJavadocVariableMissing a Javadoc comment.26
     ErrorsizesLineLengthLine is longer than 80 characters (found 81).27
     ErrorjavadocJavadocVariableMissing a Javadoc comment.27
     ErrorjavadocJavadocVariableMissing a Javadoc comment.29
     ErrorsizesLineLengthLine is longer than 80 characters (found 87).56
     ErrorsizesLineLengthLine is longer than 80 characters (found 84).68
     ErrorsizesLineLengthLine is longer than 80 characters (found 84).81
     ErrorsizesLineLengthLine is longer than 80 characters (found 84).91
     ErrorsizesLineLengthLine is longer than 80 characters (found 87).102
     ErrorsizesLineLengthLine is longer than 80 characters (found 85).112
     ErrorwhitespaceNoWhitespaceBefore',' is preceded with whitespace.116
     ErrorsizesLineLengthLine is longer than 80 characters (found 84).132
     ErrorsizesLineLengthLine is longer than 80 characters (found 85).138
     ErrorsizesLineLengthLine is longer than 80 characters (found 112).154
     ErrorsizesLineLengthLine is longer than 80 characters (found 82).176
    +

    com/studentgui/apphelpers/UiNotifier.java

    + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
    SeverityCategoryRuleMessageLine
     ErrordesignFinalClassClass UiNotifier should be declared as final.17
     ErrorjavadocJavadocVariableMissing a Javadoc comment.18
     ErrorsizesLineLengthLine is longer than 80 characters (found 100).19
     ErrorjavadocJavadocVariableMissing a Javadoc comment.19
     ErrorcodingMagicNumber'0x22' is a magic number.34
     ErrorcodingMagicNumber'0x22' is a magic number.34
     ErrorcodingMagicNumber'0x22' is a magic number.34
     ErrorcodingMagicNumber'200' is a magic number.34
     ErrorcodingMagicNumber'12' is a magic number.36
     ErrorblocksLeftCurly'{' at column 21 should have line break after.45
     ErrorcodingMagicNumber'2000' is a magic number.45
     ErrorblocksRightCurly'}' at column 43 should be on the same line as the next part of a multi-block statement (one that directly contains multiple blocks: if/else-if/else, do/while or try/catch/finally).45
     ErrorsizesLineLengthLine is longer than 80 characters (found 134).46
     ErrorblocksLeftCurly'{' at column 49 should have line break after.46
     ErrorsizesLineLengthLine is longer than 80 characters (found 111).47
     ErrorblocksLeftCurly'{' at column 50 should have line break after.47
     ErrorblocksLeftCurly'{' at column 72 should have line break after.47
     ErrorblocksRightCurly'}' at column 107 should be alone on a line.47
     ErrorregexpRegexpSinglelineLine has trailing spaces.51
    +

    com/studentgui/apphelpers/dto/AssessmentPayload.java

    + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
    SeverityCategoryRuleMessageLine
     ErrorjavadocJavadocPackageMissing package-info.java file.1
     ErrordesignVisibilityModifierVariable 'sessionId' must be private and have accessor methods.10
     ErrordesignVisibilityModifierVariable 'codes' must be private and have accessor methods.12
     ErrordesignVisibilityModifierVariable 'scores' must be private and have accessor methods.14
     ErrorwhitespaceWhitespaceAround'{' is not followed by whitespace.17
     ErrorwhitespaceWhitespaceAround'}' is not preceded with whitespace.17
     ErrorsizesLineLengthLine is longer than 80 characters (found 108).26
     ErrordesignDesignForExtensionClass 'AssessmentPayload' looks like designed for extension (can be subclassed), but the method 'getSessionId' does not have javadoc that explains how to do that safely. If class is not designed for extension consider making the class 'AssessmentPayload' final or making the method 'getSessionId' static/final/abstract/empty, or adding allowed annotation for the method.32
     ErrordesignDesignForExtensionClass 'AssessmentPayload' looks like designed for extension (can be subclassed), but the method 'toString' does not have javadoc that explains how to do that safely. If class is not designed for extension consider making the class 'AssessmentPayload' final or making the method 'toString' static/final/abstract/empty, or adding allowed annotation for the method.37
     ErrorsizesLineLengthLine is longer than 80 characters (found 142).39
    +

    com/studentgui/apphelpers/dto/ContactPayload.java

    + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
    SeverityCategoryRuleMessageLine
     ErrordesignVisibilityModifierVariable 'sessionId' must be private and have accessor methods.8
     ErrordesignVisibilityModifierVariable 'guardian' must be private and have accessor methods.10
     ErrordesignVisibilityModifierVariable 'method' must be private and have accessor methods.12
     ErrordesignVisibilityModifierVariable 'phone' must be private and have accessor methods.14
     ErrordesignVisibilityModifierVariable 'email' must be private and have accessor methods.16
     ErrordesignVisibilityModifierVariable 'response' must be private and have accessor methods.18
     ErrordesignVisibilityModifierVariable 'general' must be private and have accessor methods.20
     ErrordesignVisibilityModifierVariable 'specific' must be private and have accessor methods.22
     ErrordesignVisibilityModifierVariable 'notes' must be private and have accessor methods.24
     ErrorwhitespaceWhitespaceAround'{' is not followed by whitespace.27
     ErrorwhitespaceWhitespaceAround'}' is not preceded with whitespace.27
     ErrorsizesLineLengthLine is longer than 80 characters (found 265).42
     ErrorsizesParameterNumberMore than 7 parameters (found 9).42
     ErrordesignDesignForExtensionClass 'ContactPayload' looks like designed for extension (can be subclassed), but the method 'getSessionId' does not have javadoc that explains how to do that safely. If class is not designed for extension consider making the class 'ContactPayload' final or making the method 'getSessionId' static/final/abstract/empty, or adding allowed annotation for the method.54
     ErrorblocksLeftCurly'{' at column 31 should have line break after.55
    +

    com/studentgui/apphelpers/dto/KeyboardingPayload.java

    + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
    SeverityCategoryRuleMessageLine
     ErrordesignVisibilityModifierVariable 'sessionId' must be private and have accessor methods.8
     ErrordesignVisibilityModifierVariable 'program' must be private and have accessor methods.10
     ErrordesignVisibilityModifierVariable 'topic' must be private and have accessor methods.12
     ErrordesignVisibilityModifierVariable 'speed' must be private and have accessor methods.14
     ErrordesignVisibilityModifierVariable 'accuracy' must be private and have accessor methods.16
     ErrorwhitespaceWhitespaceAround'{' is not followed by whitespace.19
     ErrorwhitespaceWhitespaceAround'}' is not preceded with whitespace.19
     ErrorsizesLineLengthLine is longer than 80 characters (found 156).30
     ErrordesignDesignForExtensionClass 'KeyboardingPayload' looks like designed for extension (can be subclassed), but the method 'getSessionId' does not have javadoc that explains how to do that safely. If class is not designed for extension consider making the class 'KeyboardingPayload' final or making the method 'getSessionId' static/final/abstract/empty, or adding allowed annotation for the method.38
     ErrorblocksLeftCurly'{' at column 31 should have line break after.39
    +

    com/studentgui/apphelpers/dto/NotesPayload.java

    + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
    SeverityCategoryRuleMessageLine
     ErrordesignVisibilityModifierVariable 'sessionId' must be private and have accessor methods.8
     ErrordesignVisibilityModifierVariable 'notes' must be private and have accessor methods.10
     ErrorwhitespaceWhitespaceAround'{' is not followed by whitespace.13
     ErrorwhitespaceWhitespaceAround'}' is not preceded with whitespace.13
     ErrordesignDesignForExtensionClass 'NotesPayload' looks like designed for extension (can be subclassed), but the method 'getSessionId' does not have javadoc that explains how to do that safely. If class is not designed for extension consider making the class 'NotesPayload' final or making the method 'getSessionId' static/final/abstract/empty, or adding allowed annotation for the method.26
     ErrorblocksLeftCurly'{' at column 31 should have line break after.27
    +

    com/studentgui/apppages/Abacus.java

    + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
    SeverityCategoryRuleMessageLine
     ErrorsizesLineLengthLine is longer than 80 characters (found 89).26
     ErrorsizesLineLengthLine is longer than 80 characters (found 91).27
     ErrorsizesLineLengthLine is longer than 80 characters (found 117).30
     ErrorsizesLineLengthLine is longer than 80 characters (found 93).31
     ErrorsizesLineLengthLine is longer than 80 characters (found 96).32
     ErrorsizesLineLengthLine is longer than 80 characters (found 82).33
     ErrorsizesLineLengthLine is longer than 80 characters (found 82).35
     ErrorsizesLineLengthLine is longer than 80 characters (found 83).36
     ErrorsizesLineLengthLine is longer than 80 characters (found 89).37
     ErrorsizesLineLengthLine is longer than 80 characters (found 122).42
     ErrorsizesLineLengthLine is longer than 80 characters (found 137).43
     ErrorsizesLineLengthLine is longer than 80 characters (found 110).44
     ErrorsizesLineLengthLine is longer than 80 characters (found 89).45
     ErrorsizesLineLengthLine is longer than 80 characters (found 113).46
     ErrorsizesLineLengthLine is longer than 80 characters (found 99).51
     ErrorsizesLineLengthLine is longer than 80 characters (found 97).52
     ErrorsizesLineLengthLine is longer than 80 characters (found 108).53
     ErrorsizesLineLengthLine is longer than 80 characters (found 111).54
     ErrorsizesLineLengthLine is longer than 80 characters (found 101).57
     ErrorsizesLineLengthLine is longer than 80 characters (found 94).58
     ErrorsizesLineLengthLine is longer than 80 characters (found 106).59
     ErrorsizesLineLengthLine is longer than 80 characters (found 127).65
     ErrorjavadocJavadocVariableMissing a Javadoc comment.66
     ErrorsizesLineLengthLine is longer than 80 characters (found 88).83
     ErrorsizesLineLengthLine is longer than 80 characters (found 92).90
     ErrorsizesLineLengthLine is longer than 80 characters (found 95).94
     ErrorcodingHiddenField'lineGraph' hides a field.94
     ErrorsizesLineLengthLine is longer than 80 characters (found 149).95
     ErrorsizesLineLengthLine is longer than 80 characters (found 126).102
     ErrorwhitespaceWhitespaceAfter',' is not followed by whitespace.102
     ErrorwhitespaceWhitespaceAfter',' is not followed by whitespace.102
     ErrorwhitespaceWhitespaceAfter',' is not followed by whitespace.102
     ErrorwhitespaceWhitespaceAfter',' is not followed by whitespace.102
     ErrorwhitespaceWhitespaceAfter',' is not followed by whitespace.102
     ErrorwhitespaceWhitespaceAfter',' is not followed by whitespace.102
     ErrorwhitespaceWhitespaceAfter',' is not followed by whitespace.102
     ErrorsizesLineLengthLine is longer than 80 characters (found 124).103
     ErrorwhitespaceWhitespaceAfter',' is not followed by whitespace.103
     ErrorwhitespaceWhitespaceAfter',' is not followed by whitespace.103
     ErrorwhitespaceWhitespaceAfter',' is not followed by whitespace.103
     ErrorwhitespaceWhitespaceAfter',' is not followed by whitespace.103
     ErrorwhitespaceWhitespaceAfter',' is not followed by whitespace.103
     ErrorsizesLineLengthLine is longer than 80 characters (found 133).104
     ErrorwhitespaceWhitespaceAfter',' is not followed by whitespace.104
     ErrorwhitespaceWhitespaceAfter',' is not followed by whitespace.104
     ErrorwhitespaceWhitespaceAfter',' is not followed by whitespace.104
     ErrorwhitespaceWhitespaceAfter',' is not followed by whitespace.104
     ErrorwhitespaceWhitespaceAfter',' is not followed by whitespace.104
     ErrorsizesLineLengthLine is longer than 80 characters (found 154).105
     ErrorwhitespaceWhitespaceAfter',' is not followed by whitespace.105
     ErrorwhitespaceWhitespaceAfter',' is not followed by whitespace.105
     ErrorwhitespaceWhitespaceAfter',' is not followed by whitespace.105
     ErrorsizesLineLengthLine is longer than 80 characters (found 136).106
     ErrorwhitespaceWhitespaceAfter',' is not followed by whitespace.106
     ErrorwhitespaceWhitespaceAfter',' is not followed by whitespace.106
     ErrorwhitespaceWhitespaceAfter',' is not followed by whitespace.106
     ErrorsizesLineLengthLine is longer than 80 characters (found 165).107
     ErrorwhitespaceWhitespaceAfter',' is not followed by whitespace.107
     ErrorwhitespaceWhitespaceAfter',' is not followed by whitespace.107
     ErrorwhitespaceWhitespaceAfter',' is not followed by whitespace.107
     ErrorwhitespaceWhitespaceAfter',' is not followed by whitespace.107
     ErrorwhitespaceWhitespaceAfter',' is not followed by whitespace.107
     ErrorwhitespaceWhitespaceAfter',' is not followed by whitespace.107
     ErrorwhitespaceWhitespaceAfter',' is not followed by whitespace.107
     ErrorsizesLineLengthLine is longer than 80 characters (found 169).108
     ErrorwhitespaceWhitespaceAfter',' is not followed by whitespace.108
     ErrorwhitespaceWhitespaceAfter',' is not followed by whitespace.108
     ErrorwhitespaceWhitespaceAfter',' is not followed by whitespace.108
     ErrorwhitespaceWhitespaceAfter',' is not followed by whitespace.108
     ErrorwhitespaceWhitespaceAfter',' is not followed by whitespace.108
     ErrorwhitespaceWhitespaceAfter',' is not followed by whitespace.108
     ErrorwhitespaceWhitespaceAfter',' is not followed by whitespace.108
     ErrorwhitespaceWhitespaceAfter',' is not followed by whitespace.109
     ErrorwhitespaceWhitespaceAfter',' is not followed by whitespace.109
     ErrorwhitespaceWhitespaceAfter',' is not followed by whitespace.109
     ErrorcodingMagicNumber'20' is a magic number.117
     ErrorwhitespaceWhitespaceAfter',' is not followed by whitespace.117
     ErrorcodingMagicNumber'20' is a magic number.117
     ErrorwhitespaceWhitespaceAfter',' is not followed by whitespace.117
     ErrorcodingMagicNumber'20' is a magic number.117
     ErrorwhitespaceWhitespaceAfter',' is not followed by whitespace.117
     ErrorcodingMagicNumber'20' is a magic number.117
     ErrorsizesLineLengthLine is longer than 80 characters (found 93).119
     ErrorsizesLineLengthLine is longer than 80 characters (found 93).120
     ErrorsizesLineLengthLine is longer than 80 characters (found 98).121
     ErrorsizesLineLengthLine is longer than 80 characters (found 81).130
     ErrorcodingMagicNumber'16' is a magic number.130
     ErrorcodingMagicNumber'20' is a magic number.138
     ErrorsizesLineLengthLine is longer than 80 characters (found 94).143
     ErrorwhitespaceWhitespaceAround'->' is not followed by whitespace.143
     ErrorwhitespaceWhitespaceAround'->' is not preceded with whitespace.143
     ErrorsizesLineLengthLine is longer than 80 characters (found 114).144
     ErrorsizesLineLengthLine is longer than 80 characters (found 108).145
     ErrorcodingMagicNumber'320' is a magic number.145
     ErrorcodingMagicNumber'140' is a magic number.145
     ErrorcodingMagicNumber'50' is a magic number.145
     ErrorsizesLineLengthLine is longer than 80 characters (found 121).151
     ErrorsizesLineLengthLine is longer than 80 characters (found 82).154
     ErrorsizesLineLengthLine is longer than 80 characters (found 82).155
     ErrorsizesLineLengthLine is longer than 80 characters (found 82).158
     ErrorcodingMagicNumber'3' is a magic number.162
     ErrorcodingMagicNumber'4' is a magic number.169
     ErrorcodingMagicNumber'32' is a magic number.174
     ErrorsizesLineLengthLine is longer than 80 characters (found 93).175
     ErrorblocksLeftCurly'{' at column 59 should have line break after.175
     ErrorsizesLineLengthLine is longer than 80 characters (found 91).177
     ErrorsizesLineLengthLine is longer than 80 characters (found 84).178
     ErrorcodingMagicNumber'32' is a magic number.183
     ErrorsizesLineLengthLine is longer than 80 characters (found 113).185
     ErrorsizesLineLengthLine is longer than 80 characters (found 90).187
     ErrorsizesLineLengthLine is longer than 80 characters (found 98).191
     ErrorsizesLineLengthLine is longer than 80 characters (found 112).192
     ErrorsizesLineLengthLine is longer than 80 characters (found 92).224
     ErrorsizesLineLengthLine is longer than 80 characters (found 86).225
     ErrorsizesLineLengthLine is longer than 80 characters (found 82).231
     ErrorsizesLineLengthLine is longer than 80 characters (found 87).233
     ErrorsizesLineLengthLine is longer than 80 characters (found 154).248
     ErrorsizesLineLengthLine is longer than 80 characters (found 100).253
     ErrorsizesLineLengthLine is longer than 80 characters (found 92).254
     ErrorsizesLineLengthLine is longer than 80 characters (found 113).255
     ErrorsizesLineLengthLine is longer than 80 characters (found 103).263
     ErrorsizesLineLengthLine is longer than 80 characters (found 105).266
     ErrorsizesLineLengthLine is longer than 80 characters (found 148).267
     ErrorsizesLineLengthLine is longer than 80 characters (found 155).268
     ErrorsizesLineLengthLine is longer than 80 characters (found 91).270
     ErrorsizesLineLengthLine is longer than 80 characters (found 91).272
     ErrorsizesLineLengthLine is longer than 80 characters (found 119).274
     ErrorsizesLineLengthLine is longer than 80 characters (found 123).275
     ErrorsizesLineLengthLine is longer than 80 characters (found 100).278
     ErrorsizesLineLengthLine is longer than 80 characters (found 125).279
     ErrorsizesLineLengthLine is longer than 80 characters (found 89).282
     ErrorsizesLineLengthLine is longer than 80 characters (found 199).283
     ErrorsizesLineLengthLine is longer than 80 characters (found 108).287
     ErrorsizesLineLengthLine is longer than 80 characters (found 98).292
     ErrorsizesLineLengthLine is longer than 80 characters (found 88).294
     ErrorcodingMagicNumber'1000' is a magic number.294
     ErrorcodingMagicNumber'240' is a magic number.294
     ErrorsizesLineLengthLine is longer than 80 characters (found 90).295
     ErrorsizesLineLengthLine is longer than 80 characters (found 89).296
     ErrorsizesLineLengthLine is longer than 80 characters (found 95).300
     ErrorsizesLineLengthLine is longer than 80 characters (found 81).301
     ErrorsizesLineLengthLine is longer than 80 characters (found 88).307
     ErrorcodingMagicNumber'1000' is a magic number.307
     ErrorcodingMagicNumber'240' is a magic number.307
     ErrorsizesLineLengthLine is longer than 80 characters (found 159).315
     ErrorsizesLineLengthLine is longer than 80 characters (found 93).316
     ErrorsizesLineLengthLine is longer than 80 characters (found 102).318
     ErrorsizesLineLengthLine is longer than 80 characters (found 81).320
     ErrorsizesLineLengthLine is longer than 80 characters (found 81).322
     ErrorsizesLineLengthLine is longer than 80 characters (found 105).323
     ErrorsizesLineLengthLine is longer than 80 characters (found 103).324
     ErrorsizesLineLengthLine is longer than 80 characters (found 134).328
     ErrorwhitespaceWhitespaceAfter',' is not followed by whitespace.328
     ErrorwhitespaceWhitespaceAfter',' is not followed by whitespace.328
     ErrorwhitespaceWhitespaceAfter',' is not followed by whitespace.328
     ErrorwhitespaceWhitespaceAfter',' is not followed by whitespace.328
     ErrorwhitespaceWhitespaceAfter',' is not followed by whitespace.328
     ErrorwhitespaceWhitespaceAfter',' is not followed by whitespace.328
     ErrorwhitespaceWhitespaceAfter',' is not followed by whitespace.328
     ErrorsizesLineLengthLine is longer than 80 characters (found 92).330
     ErrorsizesLineLengthLine is longer than 80 characters (found 121).331
     ErrorsizesLineLengthLine is longer than 80 characters (found 100).334
     ErrorsizesLineLengthLine is longer than 80 characters (found 96).335
     ErrorsizesLineLengthLine is longer than 80 characters (found 96).339
     ErrorsizesLineLengthLine is longer than 80 characters (found 136).340
     ErrorsizesLineLengthLine is longer than 80 characters (found 403).342
     ErrorsizesLineLengthLine is longer than 80 characters (found 168).344
     ErrorsizesLineLengthLine is longer than 80 characters (found 98).346
     ErrorsizesLineLengthLine is longer than 80 characters (found 141).350
     ErrorsizesLineLengthLine is longer than 80 characters (found 112).353
     ErrorsizesLineLengthLine is longer than 80 characters (found 137).361
     ErrorsizesLineLengthLine is longer than 80 characters (found 91).362
     ErrorsizesLineLengthLine is longer than 80 characters (found 89).368
     ErrorsizesLineLengthLine is longer than 80 characters (found 92).370
     ErrorsizesLineLengthLine is longer than 80 characters (found 112).371
     ErrorsizesLineLengthLine is longer than 80 characters (found 81).374
     ErrorsizesLineLengthLine is longer than 80 characters (found 104).377
     ErrorsizesLineLengthLine is longer than 80 characters (found 149).381
     ErrorsizesLineLengthLine is longer than 80 characters (found 190).391
     ErrorsizesLineLengthLine is longer than 80 characters (found 81).397
     ErrorsizesLineLengthLine is longer than 80 characters (found 90).403
     ErrorsizesLineLengthLine is longer than 80 characters (found 83).404
     ErrordesignDesignForExtensionClass 'Abacus' looks like designed for extension (can be subclassed), but the method 'dateChanged' does not have javadoc that explains how to do that safely. If class is not designed for extension consider making the class 'Abacus' final or making the method 'dateChanged' static/final/abstract/empty, or adding allowed annotation for the method.413
     ErrordesignDesignForExtensionClass 'Abacus' looks like designed for extension (can be subclassed), but the method 'studentChanged' does not have javadoc that explains how to do that safely. If class is not designed for extension consider making the class 'Abacus' final or making the method 'studentChanged' static/final/abstract/empty, or adding allowed annotation for the method.423
     ErrorsizesLineLengthLine is longer than 80 characters (found 123).434
     ErrorregexpRegexpSinglelineLine has trailing spaces.440
    +

    com/studentgui/apppages/Braille.java

    + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
    SeverityCategoryRuleMessageLine
     ErrorsizesLineLengthLine is longer than 80 characters (found 85).27
     ErrorsizesLineLengthLine is longer than 80 characters (found 83).28
     ErrorsizesLineLengthLine is longer than 80 characters (found 89).32
     ErrorsizesLineLengthLine is longer than 80 characters (found 83).33
     ErrorsizesLineLengthLine is longer than 80 characters (found 94).34
     ErrorsizesLineLengthLine is longer than 80 characters (found 96).35
     ErrorsizesLineLengthLine is longer than 80 characters (found 101).36
     ErrorsizesLineLengthLine is longer than 80 characters (found 96).37
     ErrorsizesLineLengthLine is longer than 80 characters (found 102).38
     ErrorsizesLineLengthLine is longer than 80 characters (found 95).39
     ErrorsizesLineLengthLine is longer than 80 characters (found 135).44
     ErrorsizesLineLengthLine is longer than 80 characters (found 145).45
     ErrorsizesLineLengthLine is longer than 80 characters (found 106).46
     ErrorsizesLineLengthLine is longer than 80 characters (found 103).47
     ErrorsizesLineLengthLine is longer than 80 characters (found 104).48
     ErrorsizesLineLengthLine is longer than 80 characters (found 84).53
     ErrorsizesLineLengthLine is longer than 80 characters (found 94).54
     ErrorsizesLineLengthLine is longer than 80 characters (found 83).55
     ErrorsizesLineLengthLine is longer than 80 characters (found 125).56
     ErrorsizesLineLengthLine is longer than 80 characters (found 95).59
     ErrorsizesLineLengthLine is longer than 80 characters (found 120).60
     ErrorsizesLineLengthLine is longer than 80 characters (found 126).61
     ErrorsizesLineLengthLine is longer than 80 characters (found 128).67
     ErrorjavadocJavadocVariableMissing a Javadoc comment.68
     ErrorjavadocJavadocStyleFirst sentence should end with a period.72
     ErrorjavadocJavadocStyleFirst sentence should end with a period.74
     ErrorsizesLineLengthLine is longer than 80 characters (found 91).84
     ErrorsizesLineLengthLine is longer than 80 characters (found 82).90
     ErrorsizesLineLengthLine is longer than 80 characters (found 96).94
     ErrorcodingHiddenField'lineGraph' hides a field.94
     ErrorsizesLineLengthLine is longer than 80 characters (found 149).96
     ErrorsizesLineLengthLine is longer than 80 characters (found 168).102
     ErrorwhitespaceWhitespaceAfter',' is not followed by whitespace.102
     ErrorwhitespaceWhitespaceAfter',' is not followed by whitespace.102
     ErrorwhitespaceWhitespaceAfter',' is not followed by whitespace.102
     ErrorwhitespaceWhitespaceAfter',' is not followed by whitespace.102
     ErrorwhitespaceWhitespaceAfter',' is not followed by whitespace.102
     ErrorwhitespaceWhitespaceAfter',' is not followed by whitespace.102
     ErrorwhitespaceWhitespaceAfter',' is not followed by whitespace.102
     ErrorsizesLineLengthLine is longer than 80 characters (found 176).103
     ErrorwhitespaceWhitespaceAfter',' is not followed by whitespace.103
     ErrorwhitespaceWhitespaceAfter',' is not followed by whitespace.103
     ErrorwhitespaceWhitespaceAfter',' is not followed by whitespace.103
     ErrorwhitespaceWhitespaceAfter',' is not followed by whitespace.103
     ErrorwhitespaceWhitespaceAfter',' is not followed by whitespace.103
     ErrorwhitespaceWhitespaceAfter',' is not followed by whitespace.103
     ErrorwhitespaceWhitespaceAfter',' is not followed by whitespace.103
     ErrorsizesLineLengthLine is longer than 80 characters (found 170).104
     ErrorwhitespaceWhitespaceAfter',' is not followed by whitespace.104
     ErrorwhitespaceWhitespaceAfter',' is not followed by whitespace.104
     ErrorwhitespaceWhitespaceAfter',' is not followed by whitespace.104
     ErrorwhitespaceWhitespaceAfter',' is not followed by whitespace.104
     ErrorwhitespaceWhitespaceAfter',' is not followed by whitespace.104
     ErrorwhitespaceWhitespaceAfter',' is not followed by whitespace.104
     ErrorwhitespaceWhitespaceAfter',' is not followed by whitespace.104
     ErrorsizesLineLengthLine is longer than 80 characters (found 180).105
     ErrorwhitespaceWhitespaceAfter',' is not followed by whitespace.105
     ErrorwhitespaceWhitespaceAfter',' is not followed by whitespace.105
     ErrorwhitespaceWhitespaceAfter',' is not followed by whitespace.105
     ErrorwhitespaceWhitespaceAfter',' is not followed by whitespace.105
     ErrorwhitespaceWhitespaceAfter',' is not followed by whitespace.105
     ErrorwhitespaceWhitespaceAfter',' is not followed by whitespace.105
     ErrorwhitespaceWhitespaceAfter',' is not followed by whitespace.105
     ErrorsizesLineLengthLine is longer than 80 characters (found 141).106
     ErrorwhitespaceWhitespaceAfter',' is not followed by whitespace.106
     ErrorwhitespaceWhitespaceAfter',' is not followed by whitespace.106
     ErrorwhitespaceWhitespaceAfter',' is not followed by whitespace.106
     ErrorwhitespaceWhitespaceAfter',' is not followed by whitespace.106
     ErrorwhitespaceWhitespaceAfter',' is not followed by whitespace.106
     ErrorsizesLineLengthLine is longer than 80 characters (found 167).107
     ErrorwhitespaceWhitespaceAfter',' is not followed by whitespace.107
     ErrorwhitespaceWhitespaceAfter',' is not followed by whitespace.107
     ErrorwhitespaceWhitespaceAfter',' is not followed by whitespace.107
     ErrorwhitespaceWhitespaceAfter',' is not followed by whitespace.107
     ErrorwhitespaceWhitespaceAfter',' is not followed by whitespace.107
     ErrorwhitespaceWhitespaceAfter',' is not followed by whitespace.107
     ErrorwhitespaceWhitespaceAfter',' is not followed by whitespace.107
     ErrorsizesLineLengthLine is longer than 80 characters (found 184).108
     ErrorwhitespaceWhitespaceAfter',' is not followed by whitespace.108
     ErrorwhitespaceWhitespaceAfter',' is not followed by whitespace.108
     ErrorwhitespaceWhitespaceAfter',' is not followed by whitespace.108
     ErrorwhitespaceWhitespaceAfter',' is not followed by whitespace.108
     ErrorwhitespaceWhitespaceAfter',' is not followed by whitespace.108
     ErrorsizesLineLengthLine is longer than 80 characters (found 165).109
     ErrorwhitespaceWhitespaceAfter',' is not followed by whitespace.109
     ErrorwhitespaceWhitespaceAfter',' is not followed by whitespace.109
     ErrorwhitespaceWhitespaceAfter',' is not followed by whitespace.109
     ErrorwhitespaceWhitespaceAfter',' is not followed by whitespace.109
     ErrorwhitespaceWhitespaceAfter',' is not followed by whitespace.109
     ErrorsizesLineLengthLine is longer than 80 characters (found 168).110
     ErrorwhitespaceWhitespaceAfter',' is not followed by whitespace.110
     ErrorwhitespaceWhitespaceAfter',' is not followed by whitespace.110
     ErrorwhitespaceWhitespaceAfter',' is not followed by whitespace.110
     ErrorwhitespaceWhitespaceAfter',' is not followed by whitespace.110
     ErrorwhitespaceWhitespaceAfter',' is not followed by whitespace.110
     ErrorwhitespaceWhitespaceAfter',' is not followed by whitespace.110
     ErrorwhitespaceWhitespaceAfter',' is not followed by whitespace.110
     ErrorsizesLineLengthLine is longer than 80 characters (found 163).111
     ErrorwhitespaceWhitespaceAfter',' is not followed by whitespace.111
     ErrorwhitespaceWhitespaceAfter',' is not followed by whitespace.111
     ErrorwhitespaceWhitespaceAfter',' is not followed by whitespace.111
     ErrorwhitespaceWhitespaceAfter',' is not followed by whitespace.111
     ErrorwhitespaceWhitespaceAfter',' is not followed by whitespace.111
     ErrorwhitespaceWhitespaceAfter',' is not followed by whitespace.111
     ErrorwhitespaceWhitespaceAfter',' is not followed by whitespace.111
     ErrorsizesLineLengthLine is longer than 80 characters (found 164).112
     ErrorwhitespaceWhitespaceAfter',' is not followed by whitespace.112
     ErrorwhitespaceWhitespaceAfter',' is not followed by whitespace.112
     ErrorwhitespaceWhitespaceAfter',' is not followed by whitespace.112
     ErrorwhitespaceWhitespaceAfter',' is not followed by whitespace.112
     ErrorwhitespaceWhitespaceAfter',' is not followed by whitespace.112
     ErrorwhitespaceWhitespaceAfter',' is not followed by whitespace.112
     ErrorwhitespaceWhitespaceAfter',' is not followed by whitespace.112
     ErrorsizesLineLengthLine is longer than 80 characters (found 156).113
     ErrorwhitespaceWhitespaceAfter',' is not followed by whitespace.113
     ErrorwhitespaceWhitespaceAfter',' is not followed by whitespace.113
     ErrorwhitespaceWhitespaceAfter',' is not followed by whitespace.113
     ErrorwhitespaceWhitespaceAfter',' is not followed by whitespace.113
     ErrorwhitespaceWhitespaceAfter',' is not followed by whitespace.113
     ErrorwhitespaceWhitespaceAfter',' is not followed by whitespace.113
     ErrorwhitespaceWhitespaceAfter',' is not followed by whitespace.113
     ErrorsizesLineLengthLine is longer than 80 characters (found 133).114
     ErrorwhitespaceWhitespaceAfter',' is not followed by whitespace.114
     ErrorwhitespaceWhitespaceAfter',' is not followed by whitespace.114
     ErrorwhitespaceWhitespaceAfter',' is not followed by whitespace.114
     ErrorwhitespaceWhitespaceAfter',' is not followed by whitespace.114
     ErrorwhitespaceWhitespaceAfter',' is not followed by whitespace.114
     ErrorwhitespaceWhitespaceAfter',' is not followed by whitespace.114
     ErrorwhitespaceWhitespaceAfter',' is not followed by whitespace.114
     ErrorsizesLineLengthLine is longer than 80 characters (found 176).115
     ErrorwhitespaceWhitespaceAfter',' is not followed by whitespace.115
     ErrorwhitespaceWhitespaceAfter',' is not followed by whitespace.115
     ErrorwhitespaceWhitespaceAfter',' is not followed by whitespace.115
     ErrorwhitespaceWhitespaceAfter',' is not followed by whitespace.115
     ErrorwhitespaceWhitespaceAfter',' is not followed by whitespace.115
     ErrorwhitespaceWhitespaceAfter',' is not followed by whitespace.115
     ErrorwhitespaceWhitespaceAfter',' is not followed by whitespace.115
     ErrorsizesLineLengthLine is longer than 80 characters (found 150).116
     ErrorwhitespaceWhitespaceAfter',' is not followed by whitespace.116
     ErrorwhitespaceWhitespaceAfter',' is not followed by whitespace.116
     ErrorwhitespaceWhitespaceAfter',' is not followed by whitespace.116
     ErrorwhitespaceWhitespaceAfter',' is not followed by whitespace.116
     ErrorwhitespaceWhitespaceAfter',' is not followed by whitespace.116
     ErrorwhitespaceWhitespaceAfter',' is not followed by whitespace.116
     ErrorwhitespaceWhitespaceAfter',' is not followed by whitespace.116
     ErrorsizesLineLengthLine is longer than 80 characters (found 154).117
     ErrorwhitespaceWhitespaceAfter',' is not followed by whitespace.117
     ErrorwhitespaceWhitespaceAfter',' is not followed by whitespace.117
     ErrorwhitespaceWhitespaceAfter',' is not followed by whitespace.117
     ErrorwhitespaceWhitespaceAfter',' is not followed by whitespace.117
     ErrorwhitespaceWhitespaceAfter',' is not followed by whitespace.117
     ErrorwhitespaceWhitespaceAfter',' is not followed by whitespace.117
     ErrorwhitespaceWhitespaceAfter',' is not followed by whitespace.117
     ErrorsizesLineLengthLine is longer than 80 characters (found 121).118
     ErrorwhitespaceWhitespaceAfter',' is not followed by whitespace.118
     ErrorwhitespaceWhitespaceAfter',' is not followed by whitespace.118
     ErrorwhitespaceWhitespaceAfter',' is not followed by whitespace.118
     ErrorwhitespaceWhitespaceAfter',' is not followed by whitespace.118
     ErrorwhitespaceWhitespaceAfter',' is not followed by whitespace.118
     ErrorcodingMagicNumber'20' is a magic number.130
     ErrorwhitespaceWhitespaceAfter',' is not followed by whitespace.130
     ErrorcodingMagicNumber'20' is a magic number.130
     ErrorwhitespaceWhitespaceAfter',' is not followed by whitespace.130
     ErrorcodingMagicNumber'20' is a magic number.130
     ErrorwhitespaceWhitespaceAfter',' is not followed by whitespace.130
     ErrorcodingMagicNumber'20' is a magic number.130
     ErrorsizesLineLengthLine is longer than 80 characters (found 93).132
     ErrorsizesLineLengthLine is longer than 80 characters (found 93).133
     ErrorsizesLineLengthLine is longer than 80 characters (found 99).134
     ErrorsizesLineLengthLine is longer than 80 characters (found 85).143
     ErrorcodingMagicNumber'16' is a magic number.143
     ErrorcodingMagicNumber'20' is a magic number.151
     ErrorsizesLineLengthLine is longer than 80 characters (found 93).155
     ErrorwhitespaceWhitespaceAround'->' is not followed by whitespace.155
     ErrorwhitespaceWhitespaceAround'->' is not preceded with whitespace.155
     ErrorsizesLineLengthLine is longer than 80 characters (found 118).156
     ErrorsizesLineLengthLine is longer than 80 characters (found 112).157
     ErrorcodingMagicNumber'320' is a magic number.157
     ErrorcodingMagicNumber'140' is a magic number.157
     ErrorcodingMagicNumber'50' is a magic number.157
     ErrorsizesLineLengthLine is longer than 80 characters (found 126).163
     ErrorsizesLineLengthLine is longer than 80 characters (found 82).165
     ErrorsizesLineLengthLine is longer than 80 characters (found 87).166
     ErrorsizesLineLengthLine is longer than 80 characters (found 82).167
     ErrorcodingMagicNumber'3' is a magic number.174
     ErrorcodingMagicNumber'4' is a magic number.181
     ErrorcodingMagicNumber'32' is a magic number.187
     ErrorsizesLineLengthLine is longer than 80 characters (found 93).188
     ErrorblocksLeftCurly'{' at column 59 should have line break after.188
     ErrorsizesLineLengthLine is longer than 80 characters (found 92).190
     ErrorsizesLineLengthLine is longer than 80 characters (found 85).191
     ErrorcodingMagicNumber'32' is a magic number.196
     ErrorsizesLineLengthLine is longer than 80 characters (found 114).198
     ErrorsizesLineLengthLine is longer than 80 characters (found 91).200
     ErrorsizesLineLengthLine is longer than 80 characters (found 98).204
     ErrorsizesLineLengthLine is longer than 80 characters (found 112).205
     ErrorsizesLineLengthLine is longer than 80 characters (found 102).212
     ErrorsizesLineLengthLine is longer than 80 characters (found 93).238
     ErrorsizesLineLengthLine is longer than 80 characters (found 91).240
     ErrorsizesLineLengthLine is longer than 80 characters (found 86).257
     ErrorsizesLineLengthLine is longer than 80 characters (found 155).258
     ErrorsizesLineLengthLine is longer than 80 characters (found 105).263
     ErrorsizesLineLengthLine is longer than 80 characters (found 93).264
     ErrorsizesLineLengthLine is longer than 80 characters (found 118).265
     ErrorsizesLineLengthLine is longer than 80 characters (found 103).275
     ErrorsizesLineLengthLine is longer than 80 characters (found 148).278
     ErrorsizesLineLengthLine is longer than 80 characters (found 156).279
     ErrorsizesLineLengthLine is longer than 80 characters (found 92).281
     ErrorsizesLineLengthLine is longer than 80 characters (found 119).284
     ErrorsizesLineLengthLine is longer than 80 characters (found 123).285
     ErrorsizesLineLengthLine is longer than 80 characters (found 100).288
     ErrorsizesLineLengthLine is longer than 80 characters (found 123).289
     ErrorsizesLineLengthLine is longer than 80 characters (found 200).292
     ErrorsizesLineLengthLine is longer than 80 characters (found 103).299
     ErrorsizesLineLengthLine is longer than 80 characters (found 88).300
     ErrorcodingMagicNumber'1000' is a magic number.300
     ErrorcodingMagicNumber'240' is a magic number.300
     ErrorsizesLineLengthLine is longer than 80 characters (found 89).301
     ErrorsizesLineLengthLine is longer than 80 characters (found 99).304
     ErrorsizesLineLengthLine is longer than 80 characters (found 81).305
     ErrorsizesLineLengthLine is longer than 80 characters (found 88).311
     ErrorcodingMagicNumber'1000' is a magic number.311
     ErrorcodingMagicNumber'240' is a magic number.311
     ErrorsizesLineLengthLine is longer than 80 characters (found 159).318
     ErrorsizesLineLengthLine is longer than 80 characters (found 93).319
     ErrorsizesLineLengthLine is longer than 80 characters (found 102).321
     ErrorsizesLineLengthLine is longer than 80 characters (found 81).323
     ErrorsizesLineLengthLine is longer than 80 characters (found 81).325
     ErrorsizesLineLengthLine is longer than 80 characters (found 105).326
     ErrorsizesLineLengthLine is longer than 80 characters (found 121).331
     ErrorsizesLineLengthLine is longer than 80 characters (found 100).334
     ErrorsizesLineLengthLine is longer than 80 characters (found 96).335
     ErrorsizesLineLengthLine is longer than 80 characters (found 94).338
     ErrorsizesLineLengthLine is longer than 80 characters (found 155).339
     ErrorsizesLineLengthLine is longer than 80 characters (found 403).340
     ErrorsizesLineLengthLine is longer than 80 characters (found 168).342
     ErrorsizesLineLengthLine is longer than 80 characters (found 98).343
     ErrorsizesLineLengthLine is longer than 80 characters (found 141).347
     ErrorsizesLineLengthLine is longer than 80 characters (found 112).348
     ErrorsizesLineLengthLine is longer than 80 characters (found 86).357
     ErrorsizesLineLengthLine is longer than 80 characters (found 89).367
     ErrorsizesLineLengthLine is longer than 80 characters (found 92).368
     ErrorsizesLineLengthLine is longer than 80 characters (found 112).369
     ErrorsizesLineLengthLine is longer than 80 characters (found 89).372
     ErrorsizesLineLengthLine is longer than 80 characters (found 104).375
     ErrorsizesLineLengthLine is longer than 80 characters (found 105).377
     ErrorsizesLineLengthLine is longer than 80 characters (found 150).381
     ErrorsizesLineLengthLine is longer than 80 characters (found 150).389
     ErrorcodingMagicNumber'5' is a magic number.389
     ErrorsizesLineLengthLine is longer than 80 characters (found 128).390
     ErrorsizesLineLengthLine is longer than 80 characters (found 133).391
     ErrorsizesLineLengthLine is longer than 80 characters (found 81).395
     ErrorsizesLineLengthLine is longer than 80 characters (found 108).396
     ErrorsizesLineLengthLine is longer than 80 characters (found 127).398
     ErrorsizesLineLengthLine is longer than 80 characters (found 123).399
     ErrorsizesLineLengthLine is longer than 80 characters (found 101).400
     ErrorsizesLineLengthLine is longer than 80 characters (found 97).402
     ErrorsizesLineLengthLine is longer than 80 characters (found 114).404
     ErrorsizesLineLengthLine is longer than 80 characters (found 160).406
     ErrorsizesLineLengthLine is longer than 80 characters (found 108).407
     ErrorsizesLineLengthLine is longer than 80 characters (found 203).409
     ErrorsizesLineLengthLine is longer than 80 characters (found 90).411
     ErrordesignDesignForExtensionClass 'Braille' looks like designed for extension (can be subclassed), but the method 'dateChanged' does not have javadoc that explains how to do that safely. If class is not designed for extension consider making the class 'Braille' final or making the method 'dateChanged' static/final/abstract/empty, or adding allowed annotation for the method.423
     ErrorregexpRegexpSinglelineLine has trailing spaces.431
     ErrordesignDesignForExtensionClass 'Braille' looks like designed for extension (can be subclassed), but the method 'studentChanged' does not have javadoc that explains how to do that safely. If class is not designed for extension consider making the class 'Braille' final or making the method 'studentChanged' static/final/abstract/empty, or adding allowed annotation for the method.432
     ErrorsizesLineLengthLine is longer than 80 characters (found 84).434
     ErrorregexpRegexpSinglelineLine has trailing spaces.440
     ErrorsizesLineLengthLine is longer than 80 characters (found 119).447
     ErrorregexpRegexpSinglelineLine has trailing spaces.453
    +

    com/studentgui/apppages/BrailleNote.java

    + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
    SeverityCategoryRuleMessageLine
     ErrorregexpRegexpSinglelineLine has trailing spaces.3
     ErrorsizesLineLengthLine is longer than 80 characters (found 96).26
     ErrorsizesLineLengthLine is longer than 80 characters (found 85).27
     ErrorsizesLineLengthLine is longer than 80 characters (found 89).32
     ErrorsizesLineLengthLine is longer than 80 characters (found 96).33
     ErrorsizesLineLengthLine is longer than 80 characters (found 83).34
     ErrorsizesLineLengthLine is longer than 80 characters (found 82).36
     ErrorsizesLineLengthLine is longer than 80 characters (found 81).44
     ErrorsizesLineLengthLine is longer than 80 characters (found 86).58
     ErrorsizesLineLengthLine is longer than 80 characters (found 81).61
     ErrorsizesLineLengthLine is longer than 80 characters (found 111).68
     ErrorsizesLineLengthLine is longer than 80 characters (found 102).71
     ErrorsizesLineLengthLine is longer than 80 characters (found 101).77
     ErrorsizesLineLengthLine is longer than 80 characters (found 115).78
     ErrorsizesLineLengthLine is longer than 80 characters (found 123).79
     ErrorsizesLineLengthLine is longer than 80 characters (found 118).80
     ErrorsizesLineLengthLine is longer than 80 characters (found 83).81
     ErrorsizesLineLengthLine is longer than 80 characters (found 93).84
     ErrorsizesLineLengthLine is longer than 80 characters (found 112).85
     ErrorsizesLineLengthLine is longer than 80 characters (found 132).92
     ErrorsizesLineLengthLine is longer than 80 characters (found 81).93
     ErrorjavadocJavadocVariableMissing a Javadoc comment.93
     ErrorsizesLineLengthLine is longer than 80 characters (found 81).105
     ErrorsizesLineLengthLine is longer than 80 characters (found 91).113
     ErrorsizesLineLengthLine is longer than 80 characters (found 83).114
     ErrorsizesLineLengthLine is longer than 80 characters (found 82).117
     ErrormiscFinalParametersParameter studentName should be final.117
     ErrormiscFinalParametersParameter date should be final.117
     ErrormiscFinalParametersParameter lineGraph should be final.117
     ErrorcodingHiddenField'lineGraph' hides a field.117
     ErrorsizesLineLengthLine is longer than 80 characters (found 149).118
     ErrorsizesLineLengthLine is longer than 80 characters (found 269).124
     ErrorwhitespaceWhitespaceAfter',' is not followed by whitespace.124
     ErrorwhitespaceWhitespaceAfter',' is not followed by whitespace.124
     ErrorwhitespaceWhitespaceAfter',' is not followed by whitespace.124
     ErrorwhitespaceWhitespaceAfter',' is not followed by whitespace.124
     ErrorwhitespaceWhitespaceAfter',' is not followed by whitespace.124
     ErrorwhitespaceWhitespaceAfter',' is not followed by whitespace.124
     ErrorwhitespaceWhitespaceAfter',' is not followed by whitespace.124
     ErrorwhitespaceWhitespaceAfter',' is not followed by whitespace.124
     ErrorwhitespaceWhitespaceAfter',' is not followed by whitespace.124
     ErrorwhitespaceWhitespaceAfter',' is not followed by whitespace.124
     ErrorwhitespaceWhitespaceAfter',' is not followed by whitespace.124
     ErrorwhitespaceWhitespaceAfter',' is not followed by whitespace.124
     ErrorwhitespaceWhitespaceAfter',' is not followed by whitespace.124
     ErrorwhitespaceWhitespaceAfter',' is not followed by whitespace.124
     ErrorwhitespaceWhitespaceAfter',' is not followed by whitespace.124
     ErrorwhitespaceWhitespaceAfter',' is not followed by whitespace.124
     ErrorwhitespaceWhitespaceAfter',' is not followed by whitespace.124
     ErrorsizesLineLengthLine is longer than 80 characters (found 173).125
     ErrorwhitespaceWhitespaceAfter',' is not followed by whitespace.125
     ErrorwhitespaceWhitespaceAfter',' is not followed by whitespace.125
     ErrorwhitespaceWhitespaceAfter',' is not followed by whitespace.125
     ErrorwhitespaceWhitespaceAfter',' is not followed by whitespace.125
     ErrorwhitespaceWhitespaceAfter',' is not followed by whitespace.125
     ErrorwhitespaceWhitespaceAfter',' is not followed by whitespace.125
     ErrorwhitespaceWhitespaceAfter',' is not followed by whitespace.125
     ErrorwhitespaceWhitespaceAfter',' is not followed by whitespace.125
     ErrorwhitespaceWhitespaceAfter',' is not followed by whitespace.125
     ErrorwhitespaceWhitespaceAfter',' is not followed by whitespace.125
     ErrorwhitespaceWhitespaceAfter',' is not followed by whitespace.125
     ErrorwhitespaceWhitespaceAfter',' is not followed by whitespace.125
     ErrorwhitespaceWhitespaceAfter',' is not followed by whitespace.125
     ErrorsizesLineLengthLine is longer than 80 characters (found 204).126
     ErrorwhitespaceWhitespaceAfter',' is not followed by whitespace.126
     ErrorwhitespaceWhitespaceAfter',' is not followed by whitespace.126
     ErrorwhitespaceWhitespaceAfter',' is not followed by whitespace.126
     ErrorwhitespaceWhitespaceAfter',' is not followed by whitespace.126
     ErrorwhitespaceWhitespaceAfter',' is not followed by whitespace.126
     ErrorwhitespaceWhitespaceAfter',' is not followed by whitespace.126
     ErrorwhitespaceWhitespaceAfter',' is not followed by whitespace.126
     ErrorwhitespaceWhitespaceAfter',' is not followed by whitespace.126
     ErrorwhitespaceWhitespaceAfter',' is not followed by whitespace.126
     ErrorwhitespaceWhitespaceAfter',' is not followed by whitespace.126
     ErrorwhitespaceWhitespaceAfter',' is not followed by whitespace.126
     ErrorwhitespaceWhitespaceAfter',' is not followed by whitespace.126
     ErrorwhitespaceWhitespaceAfter',' is not followed by whitespace.126
     ErrorsizesLineLengthLine is longer than 80 characters (found 119).127
     ErrorwhitespaceWhitespaceAfter',' is not followed by whitespace.127
     ErrorwhitespaceWhitespaceAfter',' is not followed by whitespace.127
     ErrorwhitespaceWhitespaceAfter',' is not followed by whitespace.127
     ErrorwhitespaceWhitespaceAfter',' is not followed by whitespace.127
     ErrorwhitespaceWhitespaceAfter',' is not followed by whitespace.127
     ErrorsizesLineLengthLine is longer than 80 characters (found 116).128
     ErrorwhitespaceWhitespaceAfter',' is not followed by whitespace.128
     ErrorwhitespaceWhitespaceAfter',' is not followed by whitespace.128
     ErrorwhitespaceWhitespaceAfter',' is not followed by whitespace.128
     ErrorwhitespaceWhitespaceAfter',' is not followed by whitespace.128
     ErrorwhitespaceWhitespaceAfter',' is not followed by whitespace.128
     ErrorwhitespaceWhitespaceAfter',' is not followed by whitespace.128
     ErrorwhitespaceWhitespaceAfter',' is not followed by whitespace.128
     ErrorsizesLineLengthLine is longer than 80 characters (found 102).129
     ErrorwhitespaceWhitespaceAfter',' is not followed by whitespace.129
     ErrorwhitespaceWhitespaceAfter',' is not followed by whitespace.129
     ErrorwhitespaceWhitespaceAfter',' is not followed by whitespace.129
     ErrorwhitespaceWhitespaceAfter',' is not followed by whitespace.129
     ErrorwhitespaceWhitespaceAfter',' is not followed by whitespace.129
     ErrorsizesLineLengthLine is longer than 80 characters (found 117).130
     ErrorwhitespaceWhitespaceAfter',' is not followed by whitespace.130
     ErrorwhitespaceWhitespaceAfter',' is not followed by whitespace.130
     ErrorwhitespaceWhitespaceAfter',' is not followed by whitespace.130
     ErrorwhitespaceWhitespaceAfter',' is not followed by whitespace.130
     ErrorwhitespaceWhitespaceAfter',' is not followed by whitespace.130
     ErrorwhitespaceWhitespaceAfter',' is not followed by whitespace.130
     ErrorwhitespaceWhitespaceAfter',' is not followed by whitespace.130
     ErrorsizesLineLengthLine is longer than 80 characters (found 161).131
     ErrorwhitespaceWhitespaceAfter',' is not followed by whitespace.131
     ErrorwhitespaceWhitespaceAfter',' is not followed by whitespace.131
     ErrorwhitespaceWhitespaceAfter',' is not followed by whitespace.131
     ErrorwhitespaceWhitespaceAfter',' is not followed by whitespace.131
     ErrorwhitespaceWhitespaceAfter',' is not followed by whitespace.131
     ErrorwhitespaceWhitespaceAfter',' is not followed by whitespace.131
     ErrorwhitespaceWhitespaceAfter',' is not followed by whitespace.131
     ErrorwhitespaceWhitespaceAfter',' is not followed by whitespace.131
     ErrorwhitespaceWhitespaceAfter',' is not followed by whitespace.131
     ErrorsizesLineLengthLine is longer than 80 characters (found 130).132
     ErrorwhitespaceWhitespaceAfter',' is not followed by whitespace.132
     ErrorwhitespaceWhitespaceAfter',' is not followed by whitespace.132
     ErrorwhitespaceWhitespaceAfter',' is not followed by whitespace.132
     ErrorwhitespaceWhitespaceAfter',' is not followed by whitespace.132
     ErrorwhitespaceWhitespaceAfter',' is not followed by whitespace.132
     ErrorwhitespaceWhitespaceAfter',' is not followed by whitespace.132
     ErrorwhitespaceWhitespaceAfter',' is not followed by whitespace.132
     ErrorsizesLineLengthLine is longer than 80 characters (found 111).133
     ErrorwhitespaceWhitespaceAfter',' is not followed by whitespace.133
     ErrorwhitespaceWhitespaceAfter',' is not followed by whitespace.133
     ErrorwhitespaceWhitespaceAfter',' is not followed by whitespace.133
     ErrorwhitespaceWhitespaceAfter',' is not followed by whitespace.133
     ErrorwhitespaceWhitespaceAfter',' is not followed by whitespace.133
     ErrorsizesLineLengthLine is longer than 80 characters (found 148).134
     ErrorwhitespaceWhitespaceAfter',' is not followed by whitespace.134
     ErrorwhitespaceWhitespaceAfter',' is not followed by whitespace.134
     ErrorwhitespaceWhitespaceAfter',' is not followed by whitespace.134
     ErrorwhitespaceWhitespaceAfter',' is not followed by whitespace.134
     ErrorwhitespaceWhitespaceAfter',' is not followed by whitespace.134
     ErrorwhitespaceWhitespaceAfter',' is not followed by whitespace.134
     ErrorwhitespaceWhitespaceAfter',' is not followed by whitespace.134
     ErrorwhitespaceWhitespaceAfter',' is not followed by whitespace.134
     ErrorwhitespaceWhitespaceAfter',' is not followed by whitespace.134
     ErrorsizesLineLengthLine is longer than 80 characters (found 132).135
     ErrorwhitespaceWhitespaceAfter',' is not followed by whitespace.135
     ErrorwhitespaceWhitespaceAfter',' is not followed by whitespace.135
     ErrorwhitespaceWhitespaceAfter',' is not followed by whitespace.135
     ErrorwhitespaceWhitespaceAfter',' is not followed by whitespace.135
     ErrorwhitespaceWhitespaceAfter',' is not followed by whitespace.135
     ErrorwhitespaceWhitespaceAfter',' is not followed by whitespace.135
     ErrorwhitespaceWhitespaceAfter',' is not followed by whitespace.135
     ErrorsizesLineLengthLine is longer than 80 characters (found 93).142
     ErrorsizesLineLengthLine is longer than 80 characters (found 93).143
     ErrorsizesLineLengthLine is longer than 80 characters (found 103).144
     ErrorcodingMagicNumber'5' is a magic number.147
     ErrorcodingMagicNumber'5' is a magic number.147
     ErrorcodingMagicNumber'5' is a magic number.147
     ErrorcodingMagicNumber'5' is a magic number.147
     ErrorsizesLineLengthLine is longer than 80 characters (found 85).153
     ErrorcodingMagicNumber'16' is a magic number.153
     ErrorcodingMagicNumber'20' is a magic number.161
     ErrorsizesLineLengthLine is longer than 80 characters (found 92).167
     ErrorwhitespaceWhitespaceAround'->' is not followed by whitespace.167
     ErrorwhitespaceWhitespaceAround'->' is not preceded with whitespace.167
     ErrorsizesLineLengthLine is longer than 80 characters (found 113).168
     ErrorsizesLineLengthLine is longer than 80 characters (found 104).169
     ErrorcodingMagicNumber'320' is a magic number.169
     ErrorcodingMagicNumber'140' is a magic number.169
     ErrorcodingMagicNumber'50' is a magic number.169
     ErrorsizesLineLengthLine is longer than 80 characters (found 116).175
     ErrorsizesLineLengthLine is longer than 80 characters (found 82).179
     ErrorcodingMagicNumber'5' is a magic number.179
     ErrorcodingMagicNumber'5' is a magic number.179
     ErrorcodingMagicNumber'5' is a magic number.179
     ErrorcodingMagicNumber'5' is a magic number.179
     ErrorsizesLineLengthLine is longer than 80 characters (found 82).182
     ErrorcodingMagicNumber'5' is a magic number.182
     ErrorcodingMagicNumber'5' is a magic number.182
     ErrorcodingMagicNumber'5' is a magic number.182
     ErrorcodingMagicNumber'3' is a magic number.186
     ErrorcodingMagicNumber'4' is a magic number.192
     ErrorsizesLineLengthLine is longer than 80 characters (found 105).196
     ErrorcodingMagicNumber'4' is a magic number.196
     ErrorcodingMagicNumber'32' is a magic number.198
     ErrorsizesLineLengthLine is longer than 80 characters (found 93).199
     ErrorblocksLeftCurly'{' at column 59 should have line break after.199
     ErrorsizesLineLengthLine is longer than 80 characters (found 96).201
     ErrorsizesLineLengthLine is longer than 80 characters (found 89).202
     ErrorcodingMagicNumber'32' is a magic number.207
     ErrorsizesLineLengthLine is longer than 80 characters (found 118).209
     ErrorsizesLineLengthLine is longer than 80 characters (found 95).211
     ErrorsizesLineLengthLine is longer than 80 characters (found 98).215
     ErrorsizesLineLengthLine is longer than 80 characters (found 112).216
     ErrorsizesLineLengthLine is longer than 80 characters (found 97).245
     ErrorsizesLineLengthLine is longer than 80 characters (found 82).250
     ErrorsizesLineLengthLine is longer than 80 characters (found 86).262
     ErrorsizesLineLengthLine is longer than 80 characters (found 159).263
     ErrorsizesLineLengthLine is longer than 80 characters (found 105).268
     ErrorsizesLineLengthLine is longer than 80 characters (found 97).269
     ErrorsizesLineLengthLine is longer than 80 characters (found 118).270
     ErrorsizesLineLengthLine is longer than 80 characters (found 103).277
     ErrorsizesLineLengthLine is longer than 80 characters (found 81).279
     ErrorsizesLineLengthLine is longer than 80 characters (found 148).280
     ErrorsizesLineLengthLine is longer than 80 characters (found 160).281
     ErrorsizesLineLengthLine is longer than 80 characters (found 96).283
     ErrorsizesLineLengthLine is longer than 80 characters (found 119).286
     ErrorsizesLineLengthLine is longer than 80 characters (found 123).287
     ErrorsizesLineLengthLine is longer than 80 characters (found 100).290
     ErrorsizesLineLengthLine is longer than 80 characters (found 123).291
     ErrorsizesLineLengthLine is longer than 80 characters (found 204).294
     ErrorsizesLineLengthLine is longer than 80 characters (found 94).301
     ErrorsizesLineLengthLine is longer than 80 characters (found 88).302
     ErrorcodingMagicNumber'1000' is a magic number.302
     ErrorcodingMagicNumber'240' is a magic number.302
     ErrorsizesLineLengthLine is longer than 80 characters (found 89).303
     ErrorsizesLineLengthLine is longer than 80 characters (found 99).306
     ErrorsizesLineLengthLine is longer than 80 characters (found 81).307
     ErrorsizesLineLengthLine is longer than 80 characters (found 88).313
     ErrorcodingMagicNumber'1000' is a magic number.313
     ErrorcodingMagicNumber'240' is a magic number.313
     ErrorsizesLineLengthLine is longer than 80 characters (found 159).320
     ErrorsizesLineLengthLine is longer than 80 characters (found 93).321
     ErrorsizesLineLengthLine is longer than 80 characters (found 102).323
     ErrorsizesLineLengthLine is longer than 80 characters (found 81).325
     ErrorsizesLineLengthLine is longer than 80 characters (found 81).326
     ErrorsizesLineLengthLine is longer than 80 characters (found 105).327
     ErrorsizesLineLengthLine is longer than 80 characters (found 121).331
     ErrorsizesLineLengthLine is longer than 80 characters (found 100).334
     ErrorsizesLineLengthLine is longer than 80 characters (found 96).335
     ErrorsizesLineLengthLine is longer than 80 characters (found 94).338
     ErrorsizesLineLengthLine is longer than 80 characters (found 155).339
     ErrorsizesLineLengthLine is longer than 80 characters (found 403).340
     ErrorsizesLineLengthLine is longer than 80 characters (found 168).342
     ErrorsizesLineLengthLine is longer than 80 characters (found 98).343
     ErrorsizesLineLengthLine is longer than 80 characters (found 141).347
     ErrorsizesLineLengthLine is longer than 80 characters (found 112).348
     ErrorsizesLineLengthLine is longer than 80 characters (found 86).357
     ErrorsizesLineLengthLine is longer than 80 characters (found 89).367
     ErrorsizesLineLengthLine is longer than 80 characters (found 92).368
     ErrorsizesLineLengthLine is longer than 80 characters (found 112).369
     ErrorsizesLineLengthLine is longer than 80 characters (found 83).370
     ErrorsizesLineLengthLine is longer than 80 characters (found 93).372
     ErrorsizesLineLengthLine is longer than 80 characters (found 110).375
     ErrorsizesLineLengthLine is longer than 80 characters (found 154).379
     ErrorsizesLineLengthLine is longer than 80 characters (found 149).389
     ErrorcodingMagicNumber'5' is a magic number.389
     ErrorsizesLineLengthLine is longer than 80 characters (found 85).396
     ErrorsizesLineLengthLine is longer than 80 characters (found 112).397
     ErrorsizesLineLengthLine is longer than 80 characters (found 131).399
     ErrorsizesLineLengthLine is longer than 80 characters (found 127).400
     ErrorsizesLineLengthLine is longer than 80 characters (found 105).401
     ErrorsizesLineLengthLine is longer than 80 characters (found 101).403
     ErrorsizesLineLengthLine is longer than 80 characters (found 86).405
     ErrorsizesLineLengthLine is longer than 80 characters (found 157).406
     ErrorsizesLineLengthLine is longer than 80 characters (found 112).407
     ErrorsizesLineLengthLine is longer than 80 characters (found 207).409
     ErrorsizesLineLengthLine is longer than 80 characters (found 98).411
     ErrordesignDesignForExtensionClass 'BrailleNote' looks like designed for extension (can be subclassed), but the method 'dateChanged' does not have javadoc that explains how to do that safely. If class is not designed for extension consider making the class 'BrailleNote' final or making the method 'dateChanged' static/final/abstract/empty, or adding allowed annotation for the method.430
     ErrormiscFinalParametersParameter newDate should be final.431
     ErrordesignDesignForExtensionClass 'BrailleNote' looks like designed for extension (can be subclassed), but the method 'studentChanged' does not have javadoc that explains how to do that safely. If class is not designed for extension consider making the class 'BrailleNote' final or making the method 'studentChanged' static/final/abstract/empty, or adding allowed annotation for the method.439
     ErrormiscFinalParametersParameter newStudent should be final.440
     ErrorsizesLineLengthLine is longer than 80 characters (found 119).450
     ErrorregexpRegexpSinglelineLine has trailing spaces.456
    +

    com/studentgui/apppages/BrailleSense.java

    + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
    SeverityCategoryRuleMessageLine
     ErrorsizesLineLengthLine is longer than 80 characters (found 91).29
     ErrorsizesLineLengthLine is longer than 80 characters (found 90).30
     ErrorsizesLineLengthLine is longer than 80 characters (found 91).31
     ErrorsizesLineLengthLine is longer than 80 characters (found 89).34
     ErrorsizesLineLengthLine is longer than 80 characters (found 93).35
     ErrorsizesLineLengthLine is longer than 80 characters (found 87).36
     ErrorsizesLineLengthLine is longer than 80 characters (found 105).41
     ErrorsizesLineLengthLine is longer than 80 characters (found 99).42
     ErrorsizesLineLengthLine is longer than 80 characters (found 110).43
     ErrorsizesLineLengthLine is longer than 80 characters (found 86).45
     ErrorsizesLineLengthLine is longer than 80 characters (found 93).48
     ErrorsizesLineLengthLine is longer than 80 characters (found 90).49
     ErrorsizesLineLengthLine is longer than 80 characters (found 93).50
     ErrorsizesLineLengthLine is longer than 80 characters (found 98).51
     ErrorsizesLineLengthLine is longer than 80 characters (found 91).52
     ErrorsizesLineLengthLine is longer than 80 characters (found 90).57
     ErrorsizesLineLengthLine is longer than 80 characters (found 115).58
     ErrorsizesLineLengthLine is longer than 80 characters (found 125).59
     ErrorsizesLineLengthLine is longer than 80 characters (found 119).60
     ErrorsizesLineLengthLine is longer than 80 characters (found 83).61
     ErrorsizesLineLengthLine is longer than 80 characters (found 93).64
     ErrorsizesLineLengthLine is longer than 80 characters (found 103).65
     ErrorsizesLineLengthLine is longer than 80 characters (found 82).73
     ErrorjavadocJavadocVariableMissing a Javadoc comment.73
     ErrormiscFinalParametersParameter studentName should be final.92
     ErrormiscFinalParametersParameter date should be final.92
     ErrormiscFinalParametersParameter graph should be final.92
     ErrorcodingHiddenField'graph' hides a field.92
     ErrorsizesLineLengthLine is longer than 80 characters (found 149).93
     ErrorsizesLineLengthLine is longer than 80 characters (found 96).98
     ErrorsizesLineLengthLine is longer than 80 characters (found 84).102
     ErrorcodingMagicNumber'20' is a magic number.102
     ErrorcodingMagicNumber'20' is a magic number.102
     ErrorcodingMagicNumber'20' is a magic number.102
     ErrorcodingMagicNumber'20' is a magic number.102
     ErrorsizesLineLengthLine is longer than 80 characters (found 109).104
     ErrorsizesLineLengthLine is longer than 80 characters (found 109).105
     ErrorsizesLineLengthLine is longer than 80 characters (found 108).106
     ErrorsizesLineLengthLine is longer than 80 characters (found 81).115
     ErrorcodingMagicNumber'16' is a magic number.116
     ErrorcodingMagicNumber'20' is a magic number.124
     ErrorsizesLineLengthLine is longer than 80 characters (found 290).128
     ErrorsizesLineLengthLine is longer than 80 characters (found 190).129
     ErrorsizesLineLengthLine is longer than 80 characters (found 221).130
     ErrorsizesLineLengthLine is longer than 80 characters (found 128).131
     ErrorsizesLineLengthLine is longer than 80 characters (found 127).132
     ErrorsizesLineLengthLine is longer than 80 characters (found 111).133
     ErrorsizesLineLengthLine is longer than 80 characters (found 128).134
     ErrorsizesLineLengthLine is longer than 80 characters (found 174).135
     ErrorsizesLineLengthLine is longer than 80 characters (found 141).136
     ErrorsizesLineLengthLine is longer than 80 characters (found 120).137
     ErrorsizesLineLengthLine is longer than 80 characters (found 161).138
     ErrorsizesLineLengthLine is longer than 80 characters (found 143).139
     ErrorsizesLineLengthLine is longer than 80 characters (found 100).143
     ErrorsizesLineLengthLine is longer than 80 characters (found 114).144
     ErrorsizesLineLengthLine is longer than 80 characters (found 108).145
     ErrorcodingMagicNumber'360' is a magic number.145
     ErrorcodingMagicNumber'200' is a magic number.145
     ErrorcodingMagicNumber'50' is a magic number.145
     ErrorsizesLineLengthLine is longer than 80 characters (found 86).160
     ErrorcodingMagicNumber'32' is a magic number.166
     ErrorsizesLineLengthLine is longer than 80 characters (found 84).170
     ErrorcodingMagicNumber'32' is a magic number.178
     ErrorsizesLineLengthLine is longer than 80 characters (found 123).180
     ErrorsizesLineLengthLine is longer than 80 characters (found 100).182
     ErrorsizesLineLengthLine is longer than 80 characters (found 102).186
     ErrorsizesLineLengthLine is longer than 80 characters (found 116).187
     ErrorsizesLineLengthLine is longer than 80 characters (found 108).199
     ErrorsizesLineLengthLine is longer than 80 characters (found 156).208
     ErrorsizesLineLengthLine is longer than 80 characters (found 98).221
     ErrorsizesLineLengthLine is longer than 80 characters (found 82).223
     ErrorsizesLineLengthLine is longer than 80 characters (found 100).235
     ErrorsizesLineLengthLine is longer than 80 characters (found 98).236
     ErrorsizesLineLengthLine is longer than 80 characters (found 113).237
     ErrorsizesLineLengthLine is longer than 80 characters (found 103).243
     ErrorsizesLineLengthLine is longer than 80 characters (found 82).245
     ErrorsizesLineLengthLine is longer than 80 characters (found 148).246
     ErrorsizesLineLengthLine is longer than 80 characters (found 161).247
     ErrorsizesLineLengthLine is longer than 80 characters (found 97).249
     ErrorsizesLineLengthLine is longer than 80 characters (found 119).252
     ErrorsizesLineLengthLine is longer than 80 characters (found 123).253
     ErrorsizesLineLengthLine is longer than 80 characters (found 100).256
     ErrorsizesLineLengthLine is longer than 80 characters (found 123).257
     ErrorsizesLineLengthLine is longer than 80 characters (found 205).260
     ErrorsizesLineLengthLine is longer than 80 characters (found 108).262
     ErrorsizesLineLengthLine is longer than 80 characters (found 90).264
     ErrorsizesLineLengthLine is longer than 80 characters (found 84).265
     ErrorcodingMagicNumber'1000' is a magic number.265
     ErrorcodingMagicNumber'240' is a magic number.265
     ErrorsizesLineLengthLine is longer than 80 characters (found 89).266
     ErrorsizesLineLengthLine is longer than 80 characters (found 99).269
     ErrorsizesLineLengthLine is longer than 80 characters (found 81).270
     ErrorsizesLineLengthLine is longer than 80 characters (found 84).276
     ErrorcodingMagicNumber'1000' is a magic number.276
     ErrorcodingMagicNumber'240' is a magic number.276
     ErrorsizesLineLengthLine is longer than 80 characters (found 159).283
     ErrorsizesLineLengthLine is longer than 80 characters (found 93).284
     ErrorsizesLineLengthLine is longer than 80 characters (found 102).286
     ErrorsizesLineLengthLine is longer than 80 characters (found 95).288
     ErrorsizesLineLengthLine is longer than 80 characters (found 91).289
     ErrorsizesLineLengthLine is longer than 80 characters (found 81).290
     ErrorsizesLineLengthLine is longer than 80 characters (found 105).291
     ErrorsizesLineLengthLine is longer than 80 characters (found 121).295
     ErrorsizesLineLengthLine is longer than 80 characters (found 100).298
     ErrorsizesLineLengthLine is longer than 80 characters (found 96).299
     ErrorsizesLineLengthLine is longer than 80 characters (found 94).302
     ErrorsizesLineLengthLine is longer than 80 characters (found 155).303
     ErrorsizesLineLengthLine is longer than 80 characters (found 403).304
     ErrorsizesLineLengthLine is longer than 80 characters (found 168).306
     ErrorsizesLineLengthLine is longer than 80 characters (found 98).307
     ErrorsizesLineLengthLine is longer than 80 characters (found 148).311
     ErrorsizesLineLengthLine is longer than 80 characters (found 112).312
     ErrorsizesLineLengthLine is longer than 80 characters (found 86).321
     ErrorsizesLineLengthLine is longer than 80 characters (found 89).331
     ErrorsizesLineLengthLine is longer than 80 characters (found 120).332
     ErrorsizesLineLengthLine is longer than 80 characters (found 84).333
     ErrorsizesLineLengthLine is longer than 80 characters (found 94).335
     ErrorsizesLineLengthLine is longer than 80 characters (found 111).338
     ErrorsizesLineLengthLine is longer than 80 characters (found 96).345
    +

    com/studentgui/apppages/CVI.java

    + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
    SeverityCategoryRuleMessageLine
     ErrorsizesLineLengthLine is longer than 80 characters (found 92).29
     ErrorsizesLineLengthLine is longer than 80 characters (found 96).30
     ErrorsizesLineLengthLine is longer than 80 characters (found 94).36
     ErrorsizesLineLengthLine is longer than 80 characters (found 81).37
     ErrorsizesLineLengthLine is longer than 80 characters (found 81).39
     ErrorsizesLineLengthLine is longer than 80 characters (found 94).40
     ErrorsizesLineLengthLine is longer than 80 characters (found 84).47
     ErrorsizesLineLengthLine is longer than 80 characters (found 85).48
     ErrorsizesLineLengthLine is longer than 80 characters (found 88).49
     ErrorsizesLineLengthLine is longer than 80 characters (found 96).54
     ErrorsizesLineLengthLine is longer than 80 characters (found 93).55
     ErrorsizesLineLengthLine is longer than 80 characters (found 97).56
     ErrorsizesLineLengthLine is longer than 80 characters (found 92).61
     ErrorsizesLineLengthLine is longer than 80 characters (found 119).62
     ErrorsizesLineLengthLine is longer than 80 characters (found 119).63
     ErrorsizesLineLengthLine is longer than 80 characters (found 95).64
     ErrorsizesLineLengthLine is longer than 80 characters (found 93).65
     ErrorsizesLineLengthLine is longer than 80 characters (found 89).68
     ErrorsizesLineLengthLine is longer than 80 characters (found 89).69
     ErrorsizesLineLengthLine is longer than 80 characters (found 86).70
     ErrorjavadocJavadocVariableMissing a Javadoc comment.77
     ErrorsizesLineLengthLine is longer than 80 characters (found 84).81
     ErrormiscFinalParametersParameter studentName should be final.97
     ErrormiscFinalParametersParameter date should be final.97
     ErrormiscFinalParametersParameter graph should be final.97
     ErrorcodingHiddenField'graph' hides a field.97
     ErrorsizesLineLengthLine is longer than 80 characters (found 149).98
     ErrorcodingMagicNumber'20' is a magic number.105
     ErrorwhitespaceWhitespaceAfter',' is not followed by whitespace.105
     ErrorcodingMagicNumber'20' is a magic number.105
     ErrorwhitespaceWhitespaceAfter',' is not followed by whitespace.105
     ErrorcodingMagicNumber'20' is a magic number.105
     ErrorwhitespaceWhitespaceAfter',' is not followed by whitespace.105
     ErrorcodingMagicNumber'20' is a magic number.105
     ErrorsizesLineLengthLine is longer than 80 characters (found 171).107
     ErrorwhitespaceWhitespaceAround'=' is not followed by whitespace.107
     ErrorwhitespaceWhitespaceAround'=' is not preceded with whitespace.107
     ErrorwhitespaceWhitespaceAfter',' is not followed by whitespace.107
     ErrorwhitespaceWhitespaceAfter',' is not followed by whitespace.107
     ErrorwhitespaceWhitespaceAfter',' is not followed by whitespace.107
     ErrorwhitespaceWhitespaceAfter',' is not followed by whitespace.110
     ErrorcodingMagicNumber'16' is a magic number.110
     ErrorwhitespaceWhitespaceAround'=' is not followed by whitespace.113
     ErrorwhitespaceWhitespaceAround'=' is not preceded with whitespace.113
     ErrorwhitespaceWhitespaceAround'=' is not followed by whitespace.113
     ErrorwhitespaceWhitespaceAround'=' is not preceded with whitespace.113
     ErrorwhitespaceWhitespaceAround'=' is not followed by whitespace.113
     ErrorwhitespaceWhitespaceAround'=' is not preceded with whitespace.113
     ErrorsizesLineLengthLine is longer than 80 characters (found 309).115
     ErrorwhitespaceWhitespaceAfter',' is not followed by whitespace.115
     ErrorwhitespaceWhitespaceAfter',' is not followed by whitespace.115
     ErrorwhitespaceWhitespaceAfter',' is not followed by whitespace.115
     ErrorwhitespaceWhitespaceAfter',' is not followed by whitespace.115
     ErrorwhitespaceWhitespaceAfter',' is not followed by whitespace.115
     ErrorwhitespaceWhitespaceAfter',' is not followed by whitespace.115
     ErrorwhitespaceWhitespaceAfter',' is not followed by whitespace.115
     ErrorwhitespaceWhitespaceAfter',' is not followed by whitespace.115
     ErrorwhitespaceWhitespaceAfter',' is not followed by whitespace.115
     ErrorwhitespaceWhitespaceAfter',' is not followed by whitespace.115
     ErrorwhitespaceWhitespaceAfter',' is not followed by whitespace.115
     ErrorwhitespaceWhitespaceAfter',' is not followed by whitespace.115
     ErrorwhitespaceWhitespaceAfter',' is not followed by whitespace.115
     ErrorwhitespaceWhitespaceAfter',' is not followed by whitespace.115
     ErrorwhitespaceWhitespaceAfter',' is not followed by whitespace.115
     ErrorwhitespaceWhitespaceAfter',' is not followed by whitespace.115
     ErrorwhitespaceWhitespaceAfter',' is not followed by whitespace.115
     ErrorwhitespaceWhitespaceAfter',' is not followed by whitespace.115
     ErrorwhitespaceWhitespaceAfter',' is not followed by whitespace.115
     ErrorsizesLineLengthLine is longer than 80 characters (found 89).116
     ErrorwhitespaceWhitespaceAround'->' is not followed by whitespace.116
     ErrorwhitespaceWhitespaceAround'->' is not preceded with whitespace.116
     ErrorsizesLineLengthLine is longer than 80 characters (found 113).117
     ErrorsizesLineLengthLine is longer than 80 characters (found 112).118
     ErrorcodingMagicNumber'320' is a magic number.118
     ErrorcodingMagicNumber'140' is a magic number.118
     ErrorcodingMagicNumber'50' is a magic number.118
     ErrorsizesLineLengthLine is longer than 80 characters (found 83).131
     ErrorsizesLineLengthLine is longer than 80 characters (found 92).132
     ErrorcodingMagicNumber'32' is a magic number.134
     ErrorcodingMagicNumber'32' is a magic number.144
     ErrorsizesLineLengthLine is longer than 80 characters (found 117).146
     ErrorsizesLineLengthLine is longer than 80 characters (found 105).147
     ErrorblocksNeedBraces'if' construct must use '{}'s.147
     ErrorblocksEmptyBlockMust have at least one statement.148
     ErrorsizesLineLengthLine is longer than 80 characters (found 89).149
     ErrorsizesLineLengthLine is longer than 80 characters (found 108).157
     ErrorwhitespaceWhitespaceAround'->' is not followed by whitespace.157
     ErrorwhitespaceWhitespaceAround'->' is not preceded with whitespace.157
     ErrorblocksLeftCurly'{' at column 40 should have line break after.157
     ErrorwhitespaceWhitespaceAround'{' is not preceded with whitespace.157
     ErrorsizesLineLengthLine is longer than 80 characters (found 89).167
     ErrorsizesLineLengthLine is longer than 80 characters (found 82).174
     ErrorsizesLineLengthLine is longer than 80 characters (found 105).186
     ErrorsizesLineLengthLine is longer than 80 characters (found 100).191
     ErrorsizesLineLengthLine is longer than 80 characters (found 89).192
     ErrorsizesLineLengthLine is longer than 80 characters (found 113).193
     ErrorblocksNeedBraces'for' construct must use '{}'s.197
     ErrorsizesLineLengthLine is longer than 80 characters (found 103).202
     ErrorsizesLineLengthLine is longer than 80 characters (found 148).205
     ErrorsizesLineLengthLine is longer than 80 characters (found 152).206
     ErrorsizesLineLengthLine is longer than 80 characters (found 105).207
     ErrorblocksNeedBraces'if' construct must use '{}'s.207
     ErrorsizesLineLengthLine is longer than 80 characters (found 119).209
     ErrorsizesLineLengthLine is longer than 80 characters (found 123).210
     ErrorsizesLineLengthLine is longer than 80 characters (found 100).213
     ErrorsizesLineLengthLine is longer than 80 characters (found 123).214
     ErrorsizesLineLengthLine is longer than 80 characters (found 196).217
     ErrorsizesLineLengthLine is longer than 80 characters (found 99).220
     ErrorblocksNeedBraces'for' construct must use '{}'s.220
     ErrorsizesLineLengthLine is longer than 80 characters (found 90).222
     ErrorsizesLineLengthLine is longer than 80 characters (found 84).223
     ErrorcodingMagicNumber'1000' is a magic number.223
     ErrorcodingMagicNumber'240' is a magic number.223
     ErrorsizesLineLengthLine is longer than 80 characters (found 89).224
     ErrorsizesLineLengthLine is longer than 80 characters (found 99).227
     ErrorsizesLineLengthLine is longer than 80 characters (found 81).228
     ErrorsizesLineLengthLine is longer than 80 characters (found 84).234
     ErrorcodingMagicNumber'1000' is a magic number.234
     ErrorcodingMagicNumber'240' is a magic number.234
     ErrorblocksNeedBraces'if' construct must use '{}'s.237
     ErrorsizesLineLengthLine is longer than 80 characters (found 159).239
     ErrorsizesLineLengthLine is longer than 80 characters (found 93).240
     ErrorsizesLineLengthLine is longer than 80 characters (found 109).242
     ErrorsizesLineLengthLine is longer than 80 characters (found 81).244
     ErrorsizesLineLengthLine is longer than 80 characters (found 112).245
     ErrorsizesLineLengthLine is longer than 80 characters (found 121).249
     ErrorsizesLineLengthLine is longer than 80 characters (found 100).252
     ErrorsizesLineLengthLine is longer than 80 characters (found 96).253
     ErrorsizesLineLengthLine is longer than 80 characters (found 94).256
     ErrorsizesLineLengthLine is longer than 80 characters (found 155).257
     ErrorsizesLineLengthLine is longer than 80 characters (found 403).258
     ErrorsizesLineLengthLine is longer than 80 characters (found 168).260
     ErrorsizesLineLengthLine is longer than 80 characters (found 98).261
     ErrorsizesLineLengthLine is longer than 80 characters (found 148).265
     ErrorsizesLineLengthLine is longer than 80 characters (found 112).266
     ErrorsizesLineLengthLine is longer than 80 characters (found 86).275
     ErrorsizesLineLengthLine is longer than 80 characters (found 89).285
     ErrorsizesLineLengthLine is longer than 80 characters (found 120).286
     ErrorsizesLineLengthLine is longer than 80 characters (found 85).289
     ErrorsizesLineLengthLine is longer than 80 characters (found 102).292
     ErrorsizesLineLengthLine is longer than 80 characters (found 108).296
     ErrorsizesLineLengthLine is longer than 80 characters (found 82).300
    +

    com/studentgui/apppages/ContactLog.java

    + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
    SeverityCategoryRuleMessageLine
     ErrorsizesLineLengthLine is longer than 80 characters (found 91).29
     ErrorsizesLineLengthLine is longer than 80 characters (found 94).30
     ErrorsizesLineLengthLine is longer than 80 characters (found 92).31
     ErrorsizesLineLengthLine is longer than 80 characters (found 87).32
     ErrorsizesLineLengthLine is longer than 80 characters (found 88).37
     ErrorsizesLineLengthLine is longer than 80 characters (found 106).38
     ErrorsizesLineLengthLine is longer than 80 characters (found 95).39
     ErrorsizesLineLengthLine is longer than 80 characters (found 91).40
     ErrorsizesLineLengthLine is longer than 80 characters (found 125).41
     ErrorsizesLineLengthLine is longer than 80 characters (found 124).42
     ErrorsizesLineLengthLine is longer than 80 characters (found 81).43
     ErrorsizesLineLengthLine is longer than 80 characters (found 142).48
     ErrorsizesLineLengthLine is longer than 80 characters (found 130).49
     ErrorsizesLineLengthLine is longer than 80 characters (found 97).50
     ErrorsizesLineLengthLine is longer than 80 characters (found 131).55
     ErrorsizesLineLengthLine is longer than 80 characters (found 134).56
     ErrorsizesLineLengthLine is longer than 80 characters (found 121).57
     ErrorsizesLineLengthLine is longer than 80 characters (found 144).58
     ErrorsizesLineLengthLine is longer than 80 characters (found 105).61
     ErrorsizesLineLengthLine is longer than 80 characters (found 99).62
     ErrorjavadocJavadocVariableMissing a Javadoc comment.70
     ErrorsizesLineLengthLine is longer than 80 characters (found 82).71
     ErrorsizesLineLengthLine is longer than 80 characters (found 90).89
     ErrormiscFinalParametersParameter studentName should be final.102
     ErrormiscFinalParametersParameter date should be final.102
     ErrormiscFinalParametersParameter graph should be final.102
     ErrorsizesLineLengthLine is longer than 80 characters (found 149).103
     ErrorcodingMagicNumber'20' is a magic number.110
     ErrorwhitespaceWhitespaceAfter',' is not followed by whitespace.110
     ErrorcodingMagicNumber'20' is a magic number.110
     ErrorwhitespaceWhitespaceAfter',' is not followed by whitespace.110
     ErrorcodingMagicNumber'20' is a magic number.110
     ErrorwhitespaceWhitespaceAfter',' is not followed by whitespace.110
     ErrorcodingMagicNumber'20' is a magic number.110
     ErrorsizesLineLengthLine is longer than 80 characters (found 90).112
     ErrorsizesLineLengthLine is longer than 80 characters (found 171).113
     ErrorwhitespaceWhitespaceAround'=' is not followed by whitespace.113
     ErrorwhitespaceWhitespaceAround'=' is not preceded with whitespace.113
     ErrorwhitespaceWhitespaceAfter',' is not followed by whitespace.113
     ErrorwhitespaceWhitespaceAfter',' is not followed by whitespace.113
     ErrorwhitespaceWhitespaceAfter',' is not followed by whitespace.113
     ErrorsizesLineLengthLine is longer than 80 characters (found 207).114
     ErrorwhitespaceWhitespaceAfter',' is not followed by whitespace.114
     ErrorcodingMagicNumber'16' is a magic number.114
     ErrorwhitespaceWhitespaceAround'=' is not followed by whitespace.114
     ErrorwhitespaceWhitespaceAround'=' is not preceded with whitespace.114
     ErrorwhitespaceWhitespaceAround'=' is not followed by whitespace.114
     ErrorwhitespaceWhitespaceAround'=' is not preceded with whitespace.114
     ErrorwhitespaceWhitespaceAround'=' is not followed by whitespace.114
     ErrorwhitespaceWhitespaceAround'=' is not preceded with whitespace.114
     ErrorsizesLineLengthLine is longer than 80 characters (found 82).119
     ErrorsizesLineLengthLine is longer than 80 characters (found 225).120
     ErrorsizesLineLengthLine is longer than 80 characters (found 127).121
     ErrorcodingMagicNumber'24' is a magic number.121
     ErrorsizesLineLengthLine is longer than 80 characters (found 218).123
     ErrorsizesLineLengthLine is longer than 80 characters (found 188).124
     ErrorwhitespaceWhitespaceAfter',' is not followed by whitespace.124
     ErrorwhitespaceWhitespaceAfter',' is not followed by whitespace.124
     ErrorwhitespaceWhitespaceAfter',' is not followed by whitespace.124
     ErrorsizesLineLengthLine is longer than 80 characters (found 212).126
     ErrorsizesLineLengthLine is longer than 80 characters (found 115).127
     ErrorcodingMagicNumber'18' is a magic number.127
     ErrorsizesLineLengthLine is longer than 80 characters (found 213).129
     ErrorsizesLineLengthLine is longer than 80 characters (found 115).130
     ErrorcodingMagicNumber'24' is a magic number.130
     ErrorsizesLineLengthLine is longer than 80 characters (found 228).132
     ErrorsizesLineLengthLine is longer than 80 characters (found 148).133
     ErrorcodingMagicNumber'24' is a magic number.133
     ErrorsizesLineLengthLine is longer than 80 characters (found 223).135
     ErrorsizesLineLengthLine is longer than 80 characters (found 144).136
     ErrorcodingMagicNumber'24' is a magic number.136
     ErrorsizesLineLengthLine is longer than 80 characters (found 228).138
     ErrorsizesLineLengthLine is longer than 80 characters (found 148).139
     ErrorcodingMagicNumber'24' is a magic number.139
     ErrorsizesLineLengthLine is longer than 80 characters (found 224).143
     ErrorsizesLineLengthLine is longer than 80 characters (found 445).146
     ErrorcodingMagicNumber'8' is a magic number.146
     ErrorwhitespaceWhitespaceAfter',' is not followed by whitespace.146
     ErrorcodingMagicNumber'40' is a magic number.146
     ErrorsizesLineLengthLine is longer than 80 characters (found 98).151
     ErrorwhitespaceWhitespaceAround'->' is not preceded with whitespace.152
     ErrorsizesLineLengthLine is longer than 80 characters (found 81).161
     ErrorsizesLineLengthLine is longer than 80 characters (found 100).167
     ErrorwhitespaceWhitespaceAround'->' is not followed by whitespace.167
     ErrorwhitespaceWhitespaceAround'->' is not preceded with whitespace.167
     ErrorblocksLeftCurly'{' at column 40 should have line break after.167
     ErrorwhitespaceWhitespaceAround'{' is not preceded with whitespace.167
     ErrorsizesLineLengthLine is longer than 80 characters (found 141).174
     ErrorsizesLineLengthLine is longer than 80 characters (found 96).176
     ErrorsizesLineLengthLine is longer than 80 characters (found 147).192
     ErrorsizesLineLengthLine is longer than 80 characters (found 105).203
     ErrorsizesLineLengthLine is longer than 80 characters (found 96).204
     ErrorsizesLineLengthLine is longer than 80 characters (found 118).205
     ErrorsizesLineLengthLine is longer than 80 characters (found 130).217
     ErrorsizesLineLengthLine is longer than 80 characters (found 134).218
     ErrorsizesLineLengthLine is longer than 80 characters (found 121).221
     ErrorsizesLineLengthLine is longer than 80 characters (found 133).222
     ErrorsizesLineLengthLine is longer than 80 characters (found 99).226
     ErrorsizesLineLengthLine is longer than 80 characters (found 82).227
     ErrorsizesLineLengthLine is longer than 80 characters (found 191).228
     ErrorsizesLineLengthLine is longer than 80 characters (found 195).231
     ErrorsizesLineLengthLine is longer than 80 characters (found 159).232
     ErrorsizesLineLengthLine is longer than 80 characters (found 95).234
     ErrorsizesLineLengthLine is longer than 80 characters (found 174).238
    +

    com/studentgui/apppages/DigitalLiteracy.java

    + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
    SeverityCategoryRuleMessageLine
     ErrorsizesLineLengthLine is longer than 80 characters (found 91).27
     ErrorsizesLineLengthLine is longer than 80 characters (found 105).28
     ErrorsizesLineLengthLine is longer than 80 characters (found 105).33
     ErrorsizesLineLengthLine is longer than 80 characters (found 82).35
     ErrorsizesLineLengthLine is longer than 80 characters (found 83).37
     ErrorsizesLineLengthLine is longer than 80 characters (found 92).40
     ErrorsizesLineLengthLine is longer than 80 characters (found 96).46
     ErrorsizesLineLengthLine is longer than 80 characters (found 82).49
     ErrorsizesLineLengthLine is longer than 80 characters (found 81).55
     ErrorsizesLineLengthLine is longer than 80 characters (found 88).56
     ErrorsizesLineLengthLine is longer than 80 characters (found 81).63
     ErrorsizesLineLengthLine is longer than 80 characters (found 83).65
     ErrorsizesLineLengthLine is longer than 80 characters (found 82).71
     ErrorsizesLineLengthLine is longer than 80 characters (found 87).72
     ErrorsizesLineLengthLine is longer than 80 characters (found 93).75
     ErrorsizesLineLengthLine is longer than 80 characters (found 101).82
     ErrorsizesLineLengthLine is longer than 80 characters (found 115).83
     ErrorsizesLineLengthLine is longer than 80 characters (found 131).84
     ErrorsizesLineLengthLine is longer than 80 characters (found 121).85
     ErrorsizesLineLengthLine is longer than 80 characters (found 83).86
     ErrorsizesLineLengthLine is longer than 80 characters (found 93).89
     ErrorsizesLineLengthLine is longer than 80 characters (found 112).90
     ErrorsizesLineLengthLine is longer than 80 characters (found 89).93
     ErrorsizesLineLengthLine is longer than 80 characters (found 93).94
     ErrorsizesLineLengthLine is longer than 80 characters (found 85).95
     ErrorsizesLineLengthLine is longer than 80 characters (found 136).102
     ErrorsizesLineLengthLine is longer than 80 characters (found 85).103
     ErrorjavadocJavadocVariableMissing a Javadoc comment.103
     ErrorsizesLineLengthLine is longer than 80 characters (found 87).106
     ErrorsizesLineLengthLine is longer than 80 characters (found 82).112
     ErrorsizesLineLengthLine is longer than 80 characters (found 104).129
     ErrorcodingHiddenField'lineGraph' hides a field.129
     ErrorsizesLineLengthLine is longer than 80 characters (found 149).130
     ErrorsizesLineLengthLine is longer than 80 characters (found 353).136
     ErrorwhitespaceWhitespaceAfter',' is not followed by whitespace.136
     ErrorwhitespaceWhitespaceAfter',' is not followed by whitespace.136
     ErrorwhitespaceWhitespaceAfter',' is not followed by whitespace.136
     ErrorwhitespaceWhitespaceAfter',' is not followed by whitespace.136
     ErrorwhitespaceWhitespaceAfter',' is not followed by whitespace.136
     ErrorwhitespaceWhitespaceAfter',' is not followed by whitespace.136
     ErrorwhitespaceWhitespaceAfter',' is not followed by whitespace.136
     ErrorwhitespaceWhitespaceAfter',' is not followed by whitespace.136
     ErrorwhitespaceWhitespaceAfter',' is not followed by whitespace.136
     ErrorwhitespaceWhitespaceAfter',' is not followed by whitespace.136
     ErrorwhitespaceWhitespaceAfter',' is not followed by whitespace.136
     ErrorwhitespaceWhitespaceAfter',' is not followed by whitespace.136
     ErrorwhitespaceWhitespaceAfter',' is not followed by whitespace.136
     ErrorwhitespaceWhitespaceAfter',' is not followed by whitespace.136
     ErrorwhitespaceWhitespaceAfter',' is not followed by whitespace.136
     ErrorwhitespaceWhitespaceAfter',' is not followed by whitespace.136
     ErrorwhitespaceWhitespaceAfter',' is not followed by whitespace.136
     ErrorsizesLineLengthLine is longer than 80 characters (found 232).137
     ErrorwhitespaceWhitespaceAfter',' is not followed by whitespace.137
     ErrorwhitespaceWhitespaceAfter',' is not followed by whitespace.137
     ErrorwhitespaceWhitespaceAfter',' is not followed by whitespace.137
     ErrorwhitespaceWhitespaceAfter',' is not followed by whitespace.137
     ErrorwhitespaceWhitespaceAfter',' is not followed by whitespace.137
     ErrorwhitespaceWhitespaceAfter',' is not followed by whitespace.137
     ErrorwhitespaceWhitespaceAfter',' is not followed by whitespace.137
     ErrorwhitespaceWhitespaceAfter',' is not followed by whitespace.137
     ErrorwhitespaceWhitespaceAfter',' is not followed by whitespace.137
     ErrorwhitespaceWhitespaceAfter',' is not followed by whitespace.137
     ErrorwhitespaceWhitespaceAfter',' is not followed by whitespace.137
     ErrorsizesLineLengthLine is longer than 80 characters (found 121).138
     ErrorwhitespaceWhitespaceAfter',' is not followed by whitespace.138
     ErrorwhitespaceWhitespaceAfter',' is not followed by whitespace.138
     ErrorwhitespaceWhitespaceAfter',' is not followed by whitespace.138
     ErrorwhitespaceWhitespaceAfter',' is not followed by whitespace.138
     ErrorwhitespaceWhitespaceAfter',' is not followed by whitespace.138
     ErrorsizesLineLengthLine is longer than 80 characters (found 160).139
     ErrorwhitespaceWhitespaceAfter',' is not followed by whitespace.139
     ErrorwhitespaceWhitespaceAfter',' is not followed by whitespace.139
     ErrorwhitespaceWhitespaceAfter',' is not followed by whitespace.139
     ErrorwhitespaceWhitespaceAfter',' is not followed by whitespace.139
     ErrorwhitespaceWhitespaceAfter',' is not followed by whitespace.139
     ErrorwhitespaceWhitespaceAfter',' is not followed by whitespace.139
     ErrorwhitespaceWhitespaceAfter',' is not followed by whitespace.139
     ErrorwhitespaceWhitespaceAfter',' is not followed by whitespace.139
     ErrorwhitespaceWhitespaceAfter',' is not followed by whitespace.139
     ErrorsizesLineLengthLine is longer than 80 characters (found 158).140
     ErrorwhitespaceWhitespaceAfter',' is not followed by whitespace.140
     ErrorwhitespaceWhitespaceAfter',' is not followed by whitespace.140
     ErrorwhitespaceWhitespaceAfter',' is not followed by whitespace.140
     ErrorwhitespaceWhitespaceAfter',' is not followed by whitespace.140
     ErrorwhitespaceWhitespaceAfter',' is not followed by whitespace.140
     ErrorwhitespaceWhitespaceAfter',' is not followed by whitespace.140
     ErrorwhitespaceWhitespaceAfter',' is not followed by whitespace.140
     ErrorwhitespaceWhitespaceAfter',' is not followed by whitespace.140
     ErrorwhitespaceWhitespaceAfter',' is not followed by whitespace.140
     ErrorsizesLineLengthLine is longer than 80 characters (found 97).147
     ErrorsizesLineLengthLine is longer than 80 characters (found 97).148
     ErrorsizesLineLengthLine is longer than 80 characters (found 85).157
     ErrorcodingMagicNumber'16' is a magic number.157
     ErrorcodingMagicNumber'20' is a magic number.165
     ErrorsizesLineLengthLine is longer than 80 characters (found 93).170
     ErrorwhitespaceWhitespaceAround'->' is not followed by whitespace.170
     ErrorwhitespaceWhitespaceAround'->' is not preceded with whitespace.170
     ErrorsizesLineLengthLine is longer than 80 characters (found 118).171
     ErrorsizesLineLengthLine is longer than 80 characters (found 112).172
     ErrorcodingMagicNumber'320' is a magic number.172
     ErrorcodingMagicNumber'140' is a magic number.172
     ErrorcodingMagicNumber'50' is a magic number.172
     ErrorsizesLineLengthLine is longer than 80 characters (found 116).178
     ErrorsizesLineLengthLine is longer than 80 characters (found 85).181
     ErrorsizesLineLengthLine is longer than 80 characters (found 82).182
     ErrorcodingMagicNumber'5' is a magic number.182
     ErrorcodingMagicNumber'5' is a magic number.182
     ErrorcodingMagicNumber'5' is a magic number.182
     ErrorcodingMagicNumber'5' is a magic number.182
     ErrorsizesLineLengthLine is longer than 80 characters (found 82).185
     ErrorcodingMagicNumber'5' is a magic number.185
     ErrorcodingMagicNumber'5' is a magic number.185
     ErrorcodingMagicNumber'5' is a magic number.185
     ErrorcodingMagicNumber'3' is a magic number.189
     ErrorcodingMagicNumber'4' is a magic number.196
     ErrorcodingMagicNumber'32' is a magic number.201
     ErrorsizesLineLengthLine is longer than 80 characters (found 93).202
     ErrorblocksLeftCurly'{' at column 59 should have line break after.202
     ErrorsizesLineLengthLine is longer than 80 characters (found 101).203
     ErrorsizesLineLengthLine is longer than 80 characters (found 94).205
     ErrorcodingMagicNumber'32' is a magic number.211
     ErrorsizesLineLengthLine is longer than 80 characters (found 122).213
     ErrorsizesLineLengthLine is longer than 80 characters (found 99).215
     ErrorsizesLineLengthLine is longer than 80 characters (found 98).219
     ErrorsizesLineLengthLine is longer than 80 characters (found 112).220
     ErrorsizesLineLengthLine is longer than 80 characters (found 108).229
     ErrorsizesLineLengthLine is longer than 80 characters (found 101).254
     ErrorsizesLineLengthLine is longer than 80 characters (found 82).260
     ErrorsizesLineLengthLine is longer than 80 characters (found 84).262
     ErrorsizesLineLengthLine is longer than 80 characters (found 164).272
     ErrorsizesLineLengthLine is longer than 80 characters (found 100).277
     ErrorsizesLineLengthLine is longer than 80 characters (found 101).278
     ErrorsizesLineLengthLine is longer than 80 characters (found 113).279
     ErrorsizesLineLengthLine is longer than 80 characters (found 103).287
     ErrorsizesLineLengthLine is longer than 80 characters (found 86).289
     ErrorsizesLineLengthLine is longer than 80 characters (found 148).290
     ErrorsizesLineLengthLine is longer than 80 characters (found 164).291
     ErrorsizesLineLengthLine is longer than 80 characters (found 100).293
     ErrorsizesLineLengthLine is longer than 80 characters (found 119).296
     ErrorsizesLineLengthLine is longer than 80 characters (found 123).297
     ErrorsizesLineLengthLine is longer than 80 characters (found 100).300
     ErrorsizesLineLengthLine is longer than 80 characters (found 123).301
     ErrorsizesLineLengthLine is longer than 80 characters (found 81).302
     ErrorsizesLineLengthLine is longer than 80 characters (found 208).304
     ErrorsizesLineLengthLine is longer than 80 characters (found 94).311
     ErrorsizesLineLengthLine is longer than 80 characters (found 88).312
     ErrorcodingMagicNumber'1000' is a magic number.312
     ErrorcodingMagicNumber'240' is a magic number.312
     ErrorsizesLineLengthLine is longer than 80 characters (found 89).313
     ErrorsizesLineLengthLine is longer than 80 characters (found 99).316
     ErrorsizesLineLengthLine is longer than 80 characters (found 81).317
     ErrorblocksNeedBraces'for' construct must use '{}'s.318
     ErrorsizesLineLengthLine is longer than 80 characters (found 88).321
     ErrorcodingMagicNumber'1000' is a magic number.321
     ErrorcodingMagicNumber'240' is a magic number.321
     ErrorsizesLineLengthLine is longer than 80 characters (found 159).328
     ErrorsizesLineLengthLine is longer than 80 characters (found 93).329
     ErrorsizesLineLengthLine is longer than 80 characters (found 109).331
     ErrorsizesLineLengthLine is longer than 80 characters (found 81).333
     ErrorsizesLineLengthLine is longer than 80 characters (found 112).334
     ErrorsizesLineLengthLine is longer than 80 characters (found 121).338
     ErrorsizesLineLengthLine is longer than 80 characters (found 100).341
     ErrorsizesLineLengthLine is longer than 80 characters (found 96).342
     ErrorsizesLineLengthLine is longer than 80 characters (found 94).345
     ErrorsizesLineLengthLine is longer than 80 characters (found 155).346
     ErrorsizesLineLengthLine is longer than 80 characters (found 403).347
     ErrorsizesLineLengthLine is longer than 80 characters (found 168).349
     ErrorsizesLineLengthLine is longer than 80 characters (found 98).350
     ErrorsizesLineLengthLine is longer than 80 characters (found 148).354
     ErrorsizesLineLengthLine is longer than 80 characters (found 112).355
     ErrorsizesLineLengthLine is longer than 80 characters (found 86).364
     ErrorsizesLineLengthLine is longer than 80 characters (found 89).374
     ErrorsizesLineLengthLine is longer than 80 characters (found 120).375
     ErrorsizesLineLengthLine is longer than 80 characters (found 87).376
     ErrorsizesLineLengthLine is longer than 80 characters (found 97).378
     ErrorsizesLineLengthLine is longer than 80 characters (found 114).381
     ErrorsizesLineLengthLine is longer than 80 characters (found 159).385
     ErrorsizesLineLengthLine is longer than 80 characters (found 153).395
     ErrorcodingMagicNumber'5' is a magic number.395
     ErrorsizesLineLengthLine is longer than 80 characters (found 89).397
     ErrorsizesLineLengthLine is longer than 80 characters (found 85).403
     ErrorsizesLineLengthLine is longer than 80 characters (found 112).404
     ErrorsizesLineLengthLine is longer than 80 characters (found 131).406
     ErrorsizesLineLengthLine is longer than 80 characters (found 127).407
     ErrorsizesLineLengthLine is longer than 80 characters (found 105).408
     ErrorsizesLineLengthLine is longer than 80 characters (found 101).410
     ErrorsizesLineLengthLine is longer than 80 characters (found 90).412
     ErrorsizesLineLengthLine is longer than 80 characters (found 157).413
     ErrorsizesLineLengthLine is longer than 80 characters (found 112).414
     ErrorsizesLineLengthLine is longer than 80 characters (found 207).416
     ErrorsizesLineLengthLine is longer than 80 characters (found 102).418
     ErrordesignDesignForExtensionClass 'DigitalLiteracy' looks like designed for extension (can be subclassed), but the method 'dateChanged' does not have javadoc that explains how to do that safely. If class is not designed for extension consider making the class 'DigitalLiteracy' final or making the method 'dateChanged' static/final/abstract/empty, or adding allowed annotation for the method.429
     ErrordesignDesignForExtensionClass 'DigitalLiteracy' looks like designed for extension (can be subclassed), but the method 'studentChanged' does not have javadoc that explains how to do that safely. If class is not designed for extension consider making the class 'DigitalLiteracy' final or making the method 'studentChanged' static/final/abstract/empty, or adding allowed annotation for the method.438
     ErrorsizesLineLengthLine is longer than 80 characters (found 119).449
     ErrorregexpRegexpSinglelineLine has trailing spaces.455
    +

    com/studentgui/apppages/Homepage.java

    + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
    SeverityCategoryRuleMessageLine
     ErrorjavadocJavadocPackageMissing package-info.java file.1
     ErrordesignFinalClassClass Homepage should be declared as final.17
     ErrorsizesLineLengthLine is longer than 80 characters (found 82).26
     ErrorcodingMagicNumber'24f' is a magic number.27
     ErrorsizesLineLengthLine is longer than 80 characters (found 92).28
     ErrorsizesLineLengthLine is longer than 80 characters (found 87).36
     ErrorsizesLineLengthLine is longer than 80 characters (found 229).38
     ErrorsizesLineLengthLine is longer than 80 characters (found 98).41
     ErrorsizesLineLengthLine is longer than 80 characters (found 130).42
     ErrorsizesLineLengthLine is longer than 80 characters (found 189).43
     ErrorsizesLineLengthLine is longer than 80 characters (found 185).44
     ErrorsizesLineLengthLine is longer than 80 characters (found 209).45
     ErrorsizesLineLengthLine is longer than 80 characters (found 120).48
     ErrorsizesLineLengthLine is longer than 80 characters (found 131).49
     ErrorsizesLineLengthLine is longer than 80 characters (found 149).52
     ErrorsizesLineLengthLine is longer than 80 characters (found 135).53
     ErrorsizesLineLengthLine is longer than 80 characters (found 142).54
     ErrorsizesLineLengthLine is longer than 80 characters (found 93).56
     ErrorsizesLineLengthLine is longer than 80 characters (found 93).63
    +

    com/studentgui/apppages/IOS.java

    + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
    SeverityCategoryRuleMessageLine
     ErrorsizesLineLengthLine is longer than 80 characters (found 87).36
     ErrorsizesLineLengthLine is longer than 80 characters (found 87).51
     ErrorsizesLineLengthLine is longer than 80 characters (found 82).58
     ErrorsizesLineLengthLine is longer than 80 characters (found 81).67
     ErrorsizesLineLengthLine is longer than 80 characters (found 84).76
     ErrorsizesLineLengthLine is longer than 80 characters (found 98).83
     ErrorsizesLineLengthLine is longer than 80 characters (found 117).84
     ErrorsizesLineLengthLine is longer than 80 characters (found 115).85
     ErrorsizesLineLengthLine is longer than 80 characters (found 83).86
     ErrorsizesLineLengthLine is longer than 80 characters (found 93).87
     ErrorsizesLineLengthLine is longer than 80 characters (found 92).90
     ErrorsizesLineLengthLine is longer than 80 characters (found 90).91
     ErrorjavadocJavadocVariableMissing a Javadoc comment.99
     ErrormiscFinalParametersParameter studentName should be final.119
     ErrormiscFinalParametersParameter date should be final.119
     ErrormiscFinalParametersParameter graph should be final.119
     ErrorcodingHiddenField'graph' hides a field.119
     ErrorsizesLineLengthLine is longer than 80 characters (found 149).120
     ErrorcodingMagicNumber'20' is a magic number.128
     ErrorwhitespaceWhitespaceAfter',' is not followed by whitespace.128
     ErrorcodingMagicNumber'20' is a magic number.128
     ErrorwhitespaceWhitespaceAfter',' is not followed by whitespace.128
     ErrorcodingMagicNumber'20' is a magic number.128
     ErrorwhitespaceWhitespaceAfter',' is not followed by whitespace.128
     ErrorcodingMagicNumber'20' is a magic number.128
     ErrorsizesLineLengthLine is longer than 80 characters (found 82).130
     ErrorsizesLineLengthLine is longer than 80 characters (found 194).131
     ErrorwhitespaceWhitespaceAround'=' is not followed by whitespace.131
     ErrorwhitespaceWhitespaceAround'=' is not preceded with whitespace.131
     ErrorwhitespaceWhitespaceAfter',' is not followed by whitespace.131
     ErrorwhitespaceWhitespaceAfter',' is not followed by whitespace.131
     ErrorwhitespaceWhitespaceAfter',' is not followed by whitespace.131
     ErrorcodingMagicNumber'16' is a magic number.134
     ErrorwhitespaceWhitespaceAround'=' is not followed by whitespace.137
     ErrorwhitespaceWhitespaceAround'=' is not preceded with whitespace.137
     ErrorwhitespaceWhitespaceAround'=' is not followed by whitespace.137
     ErrorwhitespaceWhitespaceAround'=' is not preceded with whitespace.137
     ErrorwhitespaceWhitespaceAround'=' is not followed by whitespace.137
     ErrorwhitespaceWhitespaceAround'=' is not preceded with whitespace.137
     ErrorsizesLineLengthLine is longer than 80 characters (found 122).140
     ErrorwhitespaceWhitespaceAfter',' is not followed by whitespace.140
     ErrorwhitespaceWhitespaceAfter',' is not followed by whitespace.140
     ErrorwhitespaceWhitespaceAfter',' is not followed by whitespace.140
     ErrorwhitespaceWhitespaceAfter',' is not followed by whitespace.140
     ErrorwhitespaceWhitespaceAfter',' is not followed by whitespace.140
     ErrorsizesLineLengthLine is longer than 80 characters (found 131).141
     ErrorwhitespaceWhitespaceAfter',' is not followed by whitespace.141
     ErrorwhitespaceWhitespaceAfter',' is not followed by whitespace.141
     ErrorwhitespaceWhitespaceAfter',' is not followed by whitespace.141
     ErrorwhitespaceWhitespaceAfter',' is not followed by whitespace.141
     ErrorwhitespaceWhitespaceAfter',' is not followed by whitespace.141
     ErrorsizesLineLengthLine is longer than 80 characters (found 124).142
     ErrorwhitespaceWhitespaceAfter',' is not followed by whitespace.142
     ErrorwhitespaceWhitespaceAfter',' is not followed by whitespace.142
     ErrorwhitespaceWhitespaceAfter',' is not followed by whitespace.142
     ErrorwhitespaceWhitespaceAfter',' is not followed by whitespace.142
     ErrorwhitespaceWhitespaceAfter',' is not followed by whitespace.142
     ErrorsizesLineLengthLine is longer than 80 characters (found 110).143
     ErrorwhitespaceWhitespaceAfter',' is not followed by whitespace.143
     ErrorwhitespaceWhitespaceAfter',' is not followed by whitespace.143
     ErrorwhitespaceWhitespaceAfter',' is not followed by whitespace.143
     ErrorwhitespaceWhitespaceAfter',' is not followed by whitespace.143
     ErrorwhitespaceWhitespaceAfter',' is not followed by whitespace.143
     ErrorsizesLineLengthLine is longer than 80 characters (found 134).144
     ErrorwhitespaceWhitespaceAfter',' is not followed by whitespace.144
     ErrorwhitespaceWhitespaceAfter',' is not followed by whitespace.144
     ErrorwhitespaceWhitespaceAfter',' is not followed by whitespace.144
     ErrorwhitespaceWhitespaceAfter',' is not followed by whitespace.144
     ErrorwhitespaceWhitespaceAfter',' is not followed by whitespace.144
     ErrorsizesLineLengthLine is longer than 80 characters (found 121).145
     ErrorwhitespaceWhitespaceAfter',' is not followed by whitespace.145
     ErrorwhitespaceWhitespaceAfter',' is not followed by whitespace.145
     ErrorwhitespaceWhitespaceAfter',' is not followed by whitespace.145
     ErrorwhitespaceWhitespaceAfter',' is not followed by whitespace.145
     ErrorwhitespaceWhitespaceAfter',' is not followed by whitespace.145
     ErrorsizesLineLengthLine is longer than 80 characters (found 112).146
     ErrorwhitespaceWhitespaceAfter',' is not followed by whitespace.146
     ErrorwhitespaceWhitespaceAfter',' is not followed by whitespace.146
     ErrorwhitespaceWhitespaceAfter',' is not followed by whitespace.146
     ErrorwhitespaceWhitespaceAfter',' is not followed by whitespace.146
     ErrorwhitespaceWhitespaceAfter',' is not followed by whitespace.146
     ErrorsizesLineLengthLine is longer than 80 characters (found 129).147
     ErrorwhitespaceWhitespaceAfter',' is not followed by whitespace.147
     ErrorwhitespaceWhitespaceAfter',' is not followed by whitespace.147
     ErrorwhitespaceWhitespaceAfter',' is not followed by whitespace.147
     ErrorwhitespaceWhitespaceAfter',' is not followed by whitespace.147
     ErrorwhitespaceWhitespaceAfter',' is not followed by whitespace.147
     ErrorwhitespaceWhitespaceAfter',' is not followed by whitespace.147
     ErrorwhitespaceWhitespaceAfter',' is not followed by whitespace.147
     ErrorsizesLineLengthLine is longer than 80 characters (found 113).148
     ErrorwhitespaceWhitespaceAfter',' is not followed by whitespace.148
     ErrorwhitespaceWhitespaceAfter',' is not followed by whitespace.148
     ErrorwhitespaceWhitespaceAfter',' is not followed by whitespace.148
     ErrorwhitespaceWhitespaceAfter',' is not followed by whitespace.148
     ErrorwhitespaceWhitespaceAfter',' is not followed by whitespace.148
     ErrorsizesLineLengthLine is longer than 80 characters (found 115).149
     ErrorwhitespaceWhitespaceAfter',' is not followed by whitespace.149
     ErrorwhitespaceWhitespaceAfter',' is not followed by whitespace.149
     ErrorwhitespaceWhitespaceAfter',' is not followed by whitespace.149
     ErrorwhitespaceWhitespaceAfter',' is not followed by whitespace.149
     ErrorwhitespaceWhitespaceAfter',' is not followed by whitespace.149
     ErrorwhitespaceWhitespaceAfter',' is not followed by whitespace.149
     ErrorwhitespaceWhitespaceAfter',' is not followed by whitespace.149
     ErrorsizesLineLengthLine is longer than 80 characters (found 123).150
     ErrorwhitespaceWhitespaceAfter',' is not followed by whitespace.150
     ErrorwhitespaceWhitespaceAfter',' is not followed by whitespace.150
     ErrorwhitespaceWhitespaceAfter',' is not followed by whitespace.150
     ErrorwhitespaceWhitespaceAfter',' is not followed by whitespace.150
     ErrorwhitespaceWhitespaceAfter',' is not followed by whitespace.150
     ErrorwhitespaceWhitespaceAfter',' is not followed by whitespace.150
     ErrorwhitespaceWhitespaceAfter',' is not followed by whitespace.150
     ErrorsizesLineLengthLine is longer than 80 characters (found 135).151
     ErrorwhitespaceWhitespaceAfter',' is not followed by whitespace.151
     ErrorwhitespaceWhitespaceAfter',' is not followed by whitespace.151
     ErrorwhitespaceWhitespaceAfter',' is not followed by whitespace.151
     ErrorwhitespaceWhitespaceAfter',' is not followed by whitespace.151
     ErrorwhitespaceWhitespaceAfter',' is not followed by whitespace.151
     ErrorwhitespaceWhitespaceAfter',' is not followed by whitespace.151
     ErrorwhitespaceWhitespaceAfter',' is not followed by whitespace.151
     ErrorsizesLineLengthLine is longer than 80 characters (found 82).152
     ErrorwhitespaceWhitespaceAfter',' is not followed by whitespace.152
     ErrorwhitespaceWhitespaceAfter',' is not followed by whitespace.152
     ErrorwhitespaceWhitespaceAfter',' is not followed by whitespace.152
     ErrorwhitespaceWhitespaceAfter',' is not followed by whitespace.152
     ErrorwhitespaceWhitespaceAfter',' is not followed by whitespace.152
     ErrorsizesLineLengthLine is longer than 80 characters (found 99).155
     ErrorcodingMagicNumber'12' is a magic number.155
     ErrorsizesLineLengthLine is longer than 80 characters (found 89).156
     ErrorwhitespaceWhitespaceAround'->' is not followed by whitespace.156
     ErrorwhitespaceWhitespaceAround'->' is not preceded with whitespace.156
     ErrorsizesLineLengthLine is longer than 80 characters (found 103).157
     ErrorsizesLineLengthLine is longer than 80 characters (found 108).158
     ErrorcodingMagicNumber'360' is a magic number.158
     ErrorcodingMagicNumber'200' is a magic number.158
     ErrorcodingMagicNumber'50' is a magic number.158
     ErrorsizesLineLengthLine is longer than 80 characters (found 92).171
     ErrorcodingMagicNumber'32' is a magic number.173
     ErrorblocksLeftCurly'{' at column 47 should have line break after.174
     ErrorcodingMagicNumber'32' is a magic number.182
     ErrorsizesLineLengthLine is longer than 80 characters (found 112).184
     ErrorsizesLineLengthLine is longer than 80 characters (found 87).186
     ErrorsizesLineLengthLine is longer than 80 characters (found 98).190
     ErrorsizesLineLengthLine is longer than 80 characters (found 114).191
     ErrorsizesLineLengthLine is longer than 80 characters (found 93).197
     ErrorsizesLineLengthLine is longer than 80 characters (found 102).198
     ErrorwhitespaceWhitespaceAround'->' is not followed by whitespace.205
     ErrorwhitespaceWhitespaceAround'->' is not preceded with whitespace.205
     ErrorwhitespaceWhitespaceAround'{' is not preceded with whitespace.205
     ErrorwhitespaceWhitespaceAfter',' is not followed by whitespace.207
     ErrorsizesLineLengthLine is longer than 80 characters (found 172).212
     ErrorblocksNeedBraces'for' construct must use '{}'s.212
     ErrorsizesLineLengthLine is longer than 80 characters (found 89).225
     ErrorsizesLineLengthLine is longer than 80 characters (found 82).232
     ErrorsizesLineLengthLine is longer than 80 characters (found 86).243
     ErrorsizesLineLengthLine is longer than 80 characters (found 171).244
     ErrorsizesLineLengthLine is longer than 80 characters (found 105).249
     ErrorsizesLineLengthLine is longer than 80 characters (found 89).250
     ErrorsizesLineLengthLine is longer than 80 characters (found 118).251
     ErrorsizesLineLengthLine is longer than 80 characters (found 105).253
     ErrorcodingMultipleVariableDeclarationsOnly one variable definition per line allowed.253
     ErrorblocksNeedBraces'for' construct must use '{}'s.253
     ErrorsizesLineLengthLine is longer than 80 characters (found 103).258
     ErrorsizesLineLengthLine is longer than 80 characters (found 148).261
     ErrorsizesLineLengthLine is longer than 80 characters (found 152).262
     ErrorsizesLineLengthLine is longer than 80 characters (found 105).263
     ErrorblocksNeedBraces'if' construct must use '{}'s.263
     ErrorsizesLineLengthLine is longer than 80 characters (found 119).265
     ErrorsizesLineLengthLine is longer than 80 characters (found 123).266
     ErrorsizesLineLengthLine is longer than 80 characters (found 100).269
     ErrorsizesLineLengthLine is longer than 80 characters (found 123).270
     ErrorsizesLineLengthLine is longer than 80 characters (found 196).273
     ErrorsizesLineLengthLine is longer than 80 characters (found 90).281
     ErrorsizesLineLengthLine is longer than 80 characters (found 84).282
     ErrorcodingMagicNumber'1000' is a magic number.282
     ErrorcodingMagicNumber'240' is a magic number.282
     ErrorsizesLineLengthLine is longer than 80 characters (found 89).283
     ErrorsizesLineLengthLine is longer than 80 characters (found 99).286
     ErrorsizesLineLengthLine is longer than 80 characters (found 85).287
     ErrorsizesLineLengthLine is longer than 80 characters (found 84).293
     ErrorcodingMagicNumber'1000' is a magic number.293
     ErrorcodingMagicNumber'240' is a magic number.293
     ErrorsizesLineLengthLine is longer than 80 characters (found 159).300
     ErrorsizesLineLengthLine is longer than 80 characters (found 93).301
     ErrorsizesLineLengthLine is longer than 80 characters (found 109).303
     ErrorsizesLineLengthLine is longer than 80 characters (found 81).305
     ErrorsizesLineLengthLine is longer than 80 characters (found 112).306
     ErrorsizesLineLengthLine is longer than 80 characters (found 121).310
     ErrorsizesLineLengthLine is longer than 80 characters (found 100).313
     ErrorsizesLineLengthLine is longer than 80 characters (found 96).314
     ErrorsizesLineLengthLine is longer than 80 characters (found 94).317
     ErrorsizesLineLengthLine is longer than 80 characters (found 155).318
     ErrorsizesLineLengthLine is longer than 80 characters (found 403).319
     ErrorsizesLineLengthLine is longer than 80 characters (found 168).321
     ErrorsizesLineLengthLine is longer than 80 characters (found 98).322
     ErrorsizesLineLengthLine is longer than 80 characters (found 148).326
     ErrorsizesLineLengthLine is longer than 80 characters (found 112).327
     ErrorsizesLineLengthLine is longer than 80 characters (found 86).336
     ErrorsizesLineLengthLine is longer than 80 characters (found 89).346
     ErrorsizesLineLengthLine is longer than 80 characters (found 120).347
     ErrorsizesLineLengthLine is longer than 80 characters (found 85).350
     ErrorsizesLineLengthLine is longer than 80 characters (found 102).353
     ErrorsizesLineLengthLine is longer than 80 characters (found 171).357
     ErrorsizesLineLengthLine is longer than 80 characters (found 157).367
     ErrorcodingMagicNumber'20' is a magic number.367
     ErrorblocksNeedBraces'for' construct must use '{}'s.371
     ErrorsizesLineLengthLine is longer than 80 characters (found 95).374
     ErrorsizesLineLengthLine is longer than 80 characters (found 212).376
     ErrorsizesLineLengthLine is longer than 80 characters (found 108).378
     ErrorsizesLineLengthLine is longer than 80 characters (found 133).379
     ErrorsizesLineLengthLine is longer than 80 characters (found 89).380
     ErrorcodingMagicNumber'800' is a magic number.381
     ErrorcodingMagicNumber'400' is a magic number.381
     ErrorsizesLineLengthLine is longer than 80 characters (found 91).383
     ErrorsizesLineLengthLine is longer than 80 characters (found 106).385
     ErrorsizesLineLengthLine is longer than 80 characters (found 118).386
     ErrorblocksLeftCurly'{' at column 54 should have line break after.386
    +

    com/studentgui/apppages/InstructionalMaterials.java

    + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
    SeverityCategoryRuleMessageLine
     ErrorsizesLineLengthLine is longer than 80 characters (found 92).24
     ErrorsizesLineLengthLine is longer than 80 characters (found 88).25
     ErrorsizesLineLengthLine is longer than 80 characters (found 103).33
     ErrorsizesLineLengthLine is longer than 80 characters (found 123).38
     ErrorsizesLineLengthLine is longer than 80 characters (found 98).41
     ErrorsizesLineLengthLine is longer than 80 characters (found 98).42
     ErrorsizesLineLengthLine is longer than 80 characters (found 95).45
     ErrorsizesLineLengthLine is longer than 80 characters (found 96).46
     ErrorsizesLineLengthLine is longer than 80 characters (found 92).49
     ErrorjavadocJavadocVariableMissing a Javadoc comment.49
     ErrorcodingMagicNumber'20' is a magic number.59
     ErrorwhitespaceWhitespaceAfter',' is not followed by whitespace.59
     ErrorcodingMagicNumber'20' is a magic number.59
     ErrorwhitespaceWhitespaceAfter',' is not followed by whitespace.59
     ErrorcodingMagicNumber'20' is a magic number.59
     ErrorwhitespaceWhitespaceAfter',' is not followed by whitespace.59
     ErrorcodingMagicNumber'20' is a magic number.59
     ErrorsizesLineLengthLine is longer than 80 characters (found 91).61
     ErrorsizesLineLengthLine is longer than 80 characters (found 120).62
     ErrorwhitespaceWhitespaceAround'=' is not followed by whitespace.62
     ErrorwhitespaceWhitespaceAround'=' is not preceded with whitespace.62
     ErrorwhitespaceWhitespaceAfter',' is not followed by whitespace.62
     ErrorwhitespaceWhitespaceAfter',' is not followed by whitespace.62
     ErrorwhitespaceWhitespaceAfter',' is not followed by whitespace.62
     ErrorwhitespaceWhitespaceAround'=' is not followed by whitespace.62
     ErrorwhitespaceWhitespaceAround'=' is not preceded with whitespace.62
     ErrorwhitespaceWhitespaceAfter',' is not followed by whitespace.64
     ErrorcodingMagicNumber'16' is a magic number.64
     ErrorsizesLineLengthLine is longer than 80 characters (found 88).65
     ErrorwhitespaceWhitespaceAround'=' is not followed by whitespace.66
     ErrorwhitespaceWhitespaceAround'=' is not preceded with whitespace.66
     ErrorwhitespaceWhitespaceAround'=' is not followed by whitespace.66
     ErrorwhitespaceWhitespaceAround'=' is not preceded with whitespace.66
     ErrorsizesLineLengthLine is longer than 80 characters (found 82).68
     ErrorsizesLineLengthLine is longer than 80 characters (found 186).69
     ErrorwhitespaceWhitespaceAround'=' is not followed by whitespace.69
     ErrorwhitespaceWhitespaceAround'=' is not preceded with whitespace.69
     ErrorsizesLineLengthLine is longer than 80 characters (found 316).70
     ErrorcodingMagicNumber'20' is a magic number.70
     ErrorwhitespaceWhitespaceAfter',' is not followed by whitespace.70
     ErrorcodingMagicNumber'60' is a magic number.70
     ErrorwhitespaceWhitespaceAround'=' is not followed by whitespace.70
     ErrorwhitespaceWhitespaceAround'=' is not preceded with whitespace.70
     ErrorsizesLineLengthLine is longer than 80 characters (found 347).72
     ErrorwhitespaceWhitespaceAround'->' is not preceded with whitespace.72
     ErrorwhitespaceWhitespaceAround'=' is not followed by whitespace.72
     ErrorwhitespaceWhitespaceAround'=' is not preceded with whitespace.72
     ErrorcodingMagicNumber'3' is a magic number.72
     ErrorsizesLineLengthLine is longer than 80 characters (found 100).75
     ErrorwhitespaceWhitespaceAround'->' is not followed by whitespace.75
     ErrorwhitespaceWhitespaceAround'->' is not preceded with whitespace.75
     ErrorblocksLeftCurly'{' at column 40 should have line break after.75
     ErrorwhitespaceWhitespaceAround'{' is not preceded with whitespace.75
    +

    com/studentgui/apppages/JLineGraph.java

    + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
    SeverityCategoryRuleMessageLine
     ErrorsizesLineLengthLine is longer than 80 characters (found 94).26
     ErrorsizesLineLengthLine is longer than 80 characters (found 101).28
     ErrorsizesLineLengthLine is longer than 80 characters (found 98).29
     ErrorsizesLineLengthLine is longer than 80 characters (found 100).33
     ErrorsizesLineLengthLine is longer than 80 characters (found 96).34
     ErrorsizesLineLengthLine is longer than 80 characters (found 106).35
     ErrorsizesLineLengthLine is longer than 80 characters (found 115).36
     ErrorsizesLineLengthLine is longer than 80 characters (found 142).37
     ErrorsizesLineLengthLine is longer than 80 characters (found 99).38
     ErrorsizesLineLengthLine is longer than 80 characters (found 103).43
     ErrorsizesLineLengthLine is longer than 80 characters (found 97).45
     ErrorsizesLineLengthLine is longer than 80 characters (found 111).46
     ErrorsizesLineLengthLine is longer than 80 characters (found 103).47
     ErrorsizesLineLengthLine is longer than 80 characters (found 97).48
     ErrorsizesLineLengthLine is longer than 80 characters (found 104).51
     ErrorsizesLineLengthLine is longer than 80 characters (found 102).52
     ErrorsizesLineLengthLine is longer than 80 characters (found 89).53
     ErrorsizesLineLengthLine is longer than 80 characters (found 128).56
     ErrorsizesLineLengthLine is longer than 80 characters (found 137).57
     ErrorsizesLineLengthLine is longer than 80 characters (found 97).60
     ErrorsizesLineLengthLine is longer than 80 characters (found 95).62
     ErrorsizesLineLengthLine is longer than 80 characters (found 110).63
     ErrorsizesLineLengthLine is longer than 80 characters (found 141).70
     ErrorsizesLineLengthLine is longer than 80 characters (found 131).71
     ErrorsizesLineLengthLine is longer than 80 characters (found 121).72
     ErrorsizesLineLengthLine is longer than 80 characters (found 128).78
     ErrorsizesLineLengthLine is longer than 80 characters (found 121).79
     ErrorsizesLineLengthLine is longer than 80 characters (found 81).80
     ErrorsizesLineLengthLine is longer than 80 characters (found 92).87
     ErrorsizesLineLengthLine is longer than 80 characters (found 106).90
     ErrorsizesLineLengthLine is longer than 80 characters (found 88).91
     ErrorsizesLineLengthLine is longer than 80 characters (found 93).98
     ErrorsizesLineLengthLine is longer than 80 characters (found 100).100
     ErrorjavadocJavadocVariableMissing a Javadoc comment.100
     ErrorsizesLineLengthLine is longer than 80 characters (found 89).107
     ErrorsizesLineLengthLine is longer than 80 characters (found 99).118
     ErrorsizesLineLengthLine is longer than 80 characters (found 83).126
     ErrorjavadocJavadocMethod@return tag should be present and have description.129
     ErrorjavadocJavadocMethodExpected @param tag for 'v'.129
     ErrorsizesLineLengthLine is longer than 80 characters (found 81).136
     ErrorsizesLineLengthLine is longer than 80 characters (found 81).139
     ErrorsizesLineLengthLine is longer than 80 characters (found 103).142
     ErrorsizesLineLengthLine is longer than 80 characters (found 84).149
     ErrorsizesLineLengthLine is longer than 80 characters (found 87).151
     ErrorwhitespaceWhitespaceAfter',' is not followed by whitespace.151
     ErrorwhitespaceWhitespaceAfter',' is not followed by whitespace.151
     ErrorwhitespaceWhitespaceAfter',' is not followed by whitespace.151
     ErrorwhitespaceWhitespaceAfter',' is not followed by whitespace.151
     ErrorwhitespaceWhitespaceAfter',' is not followed by whitespace.151
     ErrorwhitespaceWhitespaceAfter',' is not followed by whitespace.151
     ErrorwhitespaceWhitespaceAfter',' is not followed by whitespace.151
     ErrorcodingMagicNumber'-0.25' is a magic number.194
     ErrorcodingMagicNumber'4.25' is a magic number.194
     ErrorcodingMagicNumber'800' is a magic number.200
     ErrorcodingMagicNumber'600' is a magic number.200
     ErrorsizesLineLengthLine is longer than 80 characters (found 87).201
     ErrorsizesLineLengthLine is longer than 80 characters (found 98).202
     ErrordesignDesignForExtensionClass 'JLineGraph' looks like designed for extension (can be subclassed), but the method 'settingsChanged' does not have javadoc that explains how to do that safely. If class is not designed for extension consider making the class 'JLineGraph' final or making the method 'settingsChanged' static/final/abstract/empty, or adding allowed annotation for the method.216
     ErrorsizesLineLengthLine is longer than 80 characters (found 117).219
     ErrorsizesLineLengthLine is longer than 80 characters (found 129).221
     ErrorsizesLineLengthLine is longer than 80 characters (found 141).223
     ErrorjavadocJavadocMethodExpected @param tag for 'plot'.251
     ErrorsizesLineLengthLine is longer than 80 characters (found 82).260
     ErrorsizesLineLengthLine is longer than 80 characters (found 81).262
     ErrorsizesLineLengthLine is longer than 80 characters (found 95).265
     ErrorjavadocJavadocMethodExpected @param tag for 'plot'.265
     ErrorjavadocJavadocMethodExpected @param tag for 'left'.265
     ErrorjavadocJavadocMethodExpected @param tag for 'right'.265
     ErrorcodingMagicNumber'255' is a magic number.267
     ErrorcodingMagicNumber'40' is a magic number.267
     ErrorcodingMagicNumber'255' is a magic number.268
     ErrorcodingMagicNumber'165' is a magic number.268
     ErrorcodingMagicNumber'40' is a magic number.268
     ErrorcodingMagicNumber'255' is a magic number.269
     ErrorcodingMagicNumber'140' is a magic number.269
     ErrorcodingMagicNumber'40' is a magic number.269
     ErrorcodingMagicNumber'255' is a magic number.270
     ErrorcodingMagicNumber'255' is a magic number.270
     ErrorcodingMagicNumber'40' is a magic number.270
     ErrorcodingMagicNumber'255' is a magic number.271
     ErrorcodingMagicNumber'40' is a magic number.271
     ErrorwhitespaceNoWhitespaceAfter'{' is followed by whitespace.274
     ErrorcodingMagicNumber'-0.25' is a magic number.274
     ErrorcodingMagicNumber'0.5' is a magic number.274
     ErrorwhitespaceNoWhitespaceAfter'{' is followed by whitespace.275
     ErrorcodingMagicNumber'0.5' is a magic number.275
     ErrorcodingMagicNumber'1.5' is a magic number.275
     ErrorwhitespaceNoWhitespaceAfter'{' is followed by whitespace.276
     ErrorcodingMagicNumber'1.5' is a magic number.276
     ErrorcodingMagicNumber'2.5' is a magic number.276
     ErrorwhitespaceNoWhitespaceAfter'{' is followed by whitespace.277
     ErrorcodingMagicNumber'2.5' is a magic number.277
     ErrorcodingMagicNumber'3.5' is a magic number.277
     ErrorwhitespaceNoWhitespaceAfter'{' is followed by whitespace.278
     ErrorcodingMagicNumber'3.5' is a magic number.278
     ErrorcodingMagicNumber'4.5' is a magic number.278
     ErrorsizesLineLengthLine is longer than 80 characters (found 99).280
     ErrorwhitespaceNoWhitespaceAfter'{' is followed by whitespace.280
     ErrorsizesLineLengthLine is longer than 80 characters (found 98).285
     ErrorwhitespaceNoWhitespaceAfter'{' is followed by whitespace.285
     ErrorsizesLineLengthLine is longer than 80 characters (found 91).286
     ErrorsizesLineLengthLine is longer than 80 characters (found 88).315
     ErrorsizesLineLengthLine is longer than 80 characters (found 108).360
     ErrorsizesLineLengthLine is longer than 80 characters (found 88).388
     ErrorcodingMagicNumber'3f' is a magic number.399
     ErrorsizesLineLengthLine is longer than 80 characters (found 97).401
     ErrorcodingMagicNumber'-6' is a magic number.401
     ErrorcodingMagicNumber'-6' is a magic number.401
     ErrorcodingMagicNumber'12' is a magic number.401
     ErrorcodingMagicNumber'12' is a magic number.401
     ErrorcodingMagicNumber'-0.25' is a magic number.408
     ErrorcodingMagicNumber'4.25' is a magic number.408
     ErrorsizesLineLengthLine is longer than 80 characters (found 112).422
     ErrorsizesLineLengthLine is longer than 80 characters (found 84).423
     ErrorsizesLineLengthLine is longer than 80 characters (found 107).425
     ErrorsizesLineLengthLine is longer than 80 characters (found 170).426
     ErrorsizesLineLengthLine is longer than 80 characters (found 111).428
     ErrorsizesLineLengthLine is longer than 80 characters (found 106).433
     ErrorsizesLineLengthLine is longer than 80 characters (found 88).436
     ErrorsizesLineLengthLine is longer than 80 characters (found 81).437
     ErrorsizesLineLengthLine is longer than 80 characters (found 116).443
     ErrorsizesLineLengthLine is longer than 80 characters (found 87).453
     ErrorsizesLineLengthLine is longer than 80 characters (found 98).458
     ErrorcodingMagicNumber'2.5f' is a magic number.481
     ErrorsizesLineLengthLine is longer than 80 characters (found 97).483
     ErrorcodingMagicNumber'-4' is a magic number.483
     ErrorcodingMagicNumber'-4' is a magic number.483
     ErrorcodingMagicNumber'8' is a magic number.483
     ErrorcodingMagicNumber'8' is a magic number.483
     ErrorcodingMagicNumber'1.5f' is a magic number.486
     ErrorsizesLineLengthLine is longer than 80 characters (found 108).491
     ErrorcodingMagicNumber'-0.25' is a magic number.494
     ErrorcodingMagicNumber'4.25' is a magic number.494
     ErrorsizesLineLengthLine is longer than 80 characters (found 81).501
     ErrorcodingMagicNumber'0.5' is a magic number.502
     ErrorcodingMagicNumber'1.5' is a magic number.502
     ErrorsizesLineLengthLine is longer than 80 characters (found 82).508
     ErrorsizesLineLengthLine is longer than 80 characters (found 85).510
     ErrorcodingMagicNumber'800' is a magic number.510
     ErrorcodingMagicNumber'100' is a magic number.510
     ErrorcodingMagicNumber'40' is a magic number.510
     ErrorsizesLineLengthLine is longer than 80 characters (found 94).511
     ErrorsizesLineLengthLine is longer than 80 characters (found 83).515
     ErrorsizesLineLengthLine is longer than 80 characters (found 82).527
     ErrorsizesLineLengthLine is longer than 80 characters (found 172).530
     ErrorsizesLineLengthLine is longer than 80 characters (found 99).531
     ErrorsizesLineLengthLine is longer than 80 characters (found 82).544
     ErrorsizesLineLengthLine is longer than 80 characters (found 85).546
     ErrorsizesLineLengthLine is longer than 80 characters (found 199).548
     ErrorsizesLineLengthLine is longer than 80 characters (found 195).549
     ErrorsizesLineLengthLine is longer than 80 characters (found 106).554
     ErrorsizesLineLengthLine is longer than 80 characters (found 88).557
     ErrorsizesLineLengthLine is longer than 80 characters (found 81).558
     ErrorsizesLineLengthLine is longer than 80 characters (found 116).564
     ErrorcodingMagicNumber'0x1b9e77' is a magic number.568
     ErrorcodingMagicNumber'0xd95f02' is a magic number.569
     ErrorcodingMagicNumber'0x7570b3' is a magic number.570
     ErrorcodingMagicNumber'0xe7298a' is a magic number.571
     ErrorcodingMagicNumber'0x66a61e' is a magic number.572
     ErrorcodingMagicNumber'0xe6ab02' is a magic number.573
     ErrorcodingMagicNumber'0xa6761d' is a magic number.574
     ErrorcodingMagicNumber'0x666666' is a magic number.575
     ErrorsizesLineLengthLine is longer than 80 characters (found 110).581
     ErrorsizesLineLengthLine is longer than 80 characters (found 146).587
     ErrorsizesLineLengthLine is longer than 80 characters (found 98).588
     ErrorsizesLineLengthLine is longer than 80 characters (found 99).589
     ErrorsizesLineLengthLine is longer than 80 characters (found 84).593
     ErrorsizesLineLengthLine is longer than 80 characters (found 161).595
     ErrorsizesLineLengthLine is longer than 80 characters (found 83).602
     ErrorsizesLineLengthLine is longer than 80 characters (found 85).618
     ErrorsizesLineLengthLine is longer than 80 characters (found 99).620
     ErrorsizesLineLengthLine is longer than 80 characters (found 87).626
     ErrorcodingMagicNumber'4' is a magic number.626
     ErrorsizesLineLengthLine is longer than 80 characters (found 101).627
     ErrorsizesLineLengthLine is longer than 80 characters (found 100).628
     ErrorsizesLineLengthLine is longer than 80 characters (found 124).630
     ErrorsizesLineLengthLine is longer than 80 characters (found 125).631
     ErrorsizesLineLengthLine is longer than 80 characters (found 85).632
     ErrorsizesLineLengthLine is longer than 80 characters (found 93).635
     ErrorsizesLineLengthLine is longer than 80 characters (found 141).637
     ErrorsizesLineLengthLine is longer than 80 characters (found 93).644
     ErrorcodingMagicNumber'-3' is a magic number.644
     ErrorcodingMagicNumber'-3' is a magic number.644
     ErrorcodingMagicNumber'6' is a magic number.644
     ErrorcodingMagicNumber'6' is a magic number.644
     ErrorsizesLineLengthLine is longer than 80 characters (found 108).648
     ErrorcodingMagicNumber'-0.25' is a magic number.651
     ErrorcodingMagicNumber'4.25' is a magic number.651
     ErrorsizesLineLengthLine is longer than 80 characters (found 82).652
     ErrorsizesLineLengthLine is longer than 80 characters (found 84).654
     ErrorsizesLineLengthLine is longer than 80 characters (found 108).657
     ErrorsizesLineLengthLine is longer than 80 characters (found 110).659
     ErrorsizesLineLengthLine is longer than 80 characters (found 92).660
     ErrorsizesLineLengthLine is longer than 80 characters (found 81).662
     ErrorcodingMagicNumber'4' is a magic number.668
     ErrorsizesLineLengthLine is longer than 80 characters (found 104).669
     ErrorsizesLineLengthLine is longer than 80 characters (found 106).670
     ErrorsizesLineLengthLine is longer than 80 characters (found 82).672
     ErrorsizesLineLengthLine is longer than 80 characters (found 126).673
     ErrorsizesLineLengthLine is longer than 80 characters (found 88).679
     ErrorsizesLineLengthLine is longer than 80 characters (found 90).681
     ErrorsizesLineLengthLine is longer than 80 characters (found 86).686
     ErrorcodingMagicNumber'1000' is a magic number.686
     ErrorcodingMagicNumber'180' is a magic number.686
     ErrorcodingMagicNumber'40' is a magic number.686
     ErrorsizesLineLengthLine is longer than 80 characters (found 94).687
     ErrorsizesLineLengthLine is longer than 80 characters (found 83).691
     ErrorsizesLineLengthLine is longer than 80 characters (found 195).709
     ErrorsizesLineLengthLine is longer than 80 characters (found 88).710
     ErrorsizesLineLengthLine is longer than 80 characters (found 90).715
     ErrorsizesLineLengthLine is longer than 80 characters (found 81).720
     ErrorwhitespaceWhitespaceAround'+' is not followed by whitespace.720
     ErrorwhitespaceWhitespaceAround'+' is not preceded with whitespace.720
     ErrorcodingMagicNumber'100' is a magic number.721
     ErrorsizesLineLengthLine is longer than 80 characters (found 134).724
     ErrorsizesLineLengthLine is longer than 80 characters (found 81).730
     ErrorsizesLineLengthLine is longer than 80 characters (found 85).731
     ErrorsizesLineLengthLine is longer than 80 characters (found 114).732
     ErrorsizesLineLengthLine is longer than 80 characters (found 83).748
     ErrorsizesLineLengthLine is longer than 80 characters (found 108).754
     ErrorsizesLineLengthLine is longer than 80 characters (found 126).769
     ErrorsizesLineLengthLine is longer than 80 characters (found 84).780
     ErrorsizesLineLengthLine is longer than 80 characters (found 89).781
     ErrorsizesLineLengthLine is longer than 80 characters (found 110).785
     ErrorsizesLineLengthLine is longer than 80 characters (found 106).799
     ErrorsizesLineLengthLine is longer than 80 characters (found 91).800
     ErrorsizesLineLengthLine is longer than 80 characters (found 114).801
     ErrorsizesLineLengthLine is longer than 80 characters (found 95).804
     ErrorsizesLineLengthLine is longer than 80 characters (found 121).808
     ErrorsizesLineLengthLine is longer than 80 characters (found 160).809
     ErrorcodingMagicNumber'6' is a magic number.822
     ErrorwhitespaceOperatorWrap'||' should be on a new line.822
     ErrorcodingMagicNumber'4' is a magic number.823
     ErrorwhitespaceOperatorWrap'||' should be on a new line.823
     ErrorcodingMagicNumber'3' is a magic number.824
     ErrorcodingMagicNumber'11' is a magic number.824
     ErrorwhitespaceOperatorWrap'||' should be on a new line.824
     ErrorcodingMagicNumber'4' is a magic number.825
     ErrorcodingMagicNumber'7' is a magic number.825
     ErrorcodingMagicNumber'8' is a magic number.834
     ErrormiscFinalParametersParameter value should be final.837
    +

    com/studentgui/apppages/Keyboarding.java

    + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
    SeverityCategoryRuleMessageLine
     ErrorsizesLineLengthLine is longer than 80 characters (found 82).28
     ErrorsizesLineLengthLine is longer than 80 characters (found 113).32
     ErrorsizesLineLengthLine is longer than 80 characters (found 114).33
     ErrorsizesLineLengthLine is longer than 80 characters (found 85).34
     ErrorsizesLineLengthLine is longer than 80 characters (found 140).40
     ErrorsizesLineLengthLine is longer than 80 characters (found 123).41
     ErrorsizesLineLengthLine is longer than 80 characters (found 112).42
     ErrorsizesLineLengthLine is longer than 80 characters (found 90).47
     ErrorsizesLineLengthLine is longer than 80 characters (found 81).49
     ErrorsizesLineLengthLine is longer than 80 characters (found 96).52
     ErrorsizesLineLengthLine is longer than 80 characters (found 101).54
     ErrorsizesLineLengthLine is longer than 80 characters (found 132).60
     ErrorsizesLineLengthLine is longer than 80 characters (found 81).61
     ErrorjavadocJavadocVariableMissing a Javadoc comment.61
     ErrorsizesLineLengthLine is longer than 80 characters (found 81).63
     ErrorcodingMultipleVariableDeclarationsEach variable declaration must be in its own statement.63
     ErrorsizesLineLengthLine is longer than 80 characters (found 82).65
     ErrorsizesLineLengthLine is longer than 80 characters (found 82).85
     ErrormiscFinalParametersParameter studentName should be final.85
     ErrormiscFinalParametersParameter date should be final.85
     ErrormiscFinalParametersParameter lineGraph should be final.85
     ErrorcodingHiddenField'lineGraph' hides a field.85
     ErrorsizesLineLengthLine is longer than 80 characters (found 149).86
     ErrorcodingMagicNumber'20' is a magic number.94
     ErrorwhitespaceWhitespaceAfter',' is not followed by whitespace.94
     ErrorcodingMagicNumber'20' is a magic number.94
     ErrorwhitespaceWhitespaceAfter',' is not followed by whitespace.94
     ErrorcodingMagicNumber'20' is a magic number.94
     ErrorwhitespaceWhitespaceAfter',' is not followed by whitespace.94
     ErrorcodingMagicNumber'20' is a magic number.94
     ErrorsizesLineLengthLine is longer than 80 characters (found 90).96
     ErrorcodingMagicNumber'20' is a magic number.97
     ErrorwhitespaceWhitespaceAfter',' is not followed by whitespace.97
     ErrorcodingMagicNumber'20' is a magic number.97
     ErrorwhitespaceWhitespaceAfter',' is not followed by whitespace.97
     ErrorcodingMagicNumber'20' is a magic number.97
     ErrorwhitespaceWhitespaceAfter',' is not followed by whitespace.97
     ErrorcodingMagicNumber'20' is a magic number.97
     ErrorsizesLineLengthLine is longer than 80 characters (found 175).98
     ErrorwhitespaceWhitespaceAround'=' is not followed by whitespace.98
     ErrorwhitespaceWhitespaceAround'=' is not preceded with whitespace.98
     ErrorwhitespaceWhitespaceAfter',' is not followed by whitespace.98
     ErrorwhitespaceWhitespaceAfter',' is not followed by whitespace.98
     ErrorwhitespaceWhitespaceAfter',' is not followed by whitespace.98
     ErrorwhitespaceWhitespaceAfter',' is not followed by whitespace.100
     ErrorcodingMagicNumber'16' is a magic number.100
     ErrorsizesLineLengthLine is longer than 80 characters (found 89).101
     ErrorwhitespaceWhitespaceAround'=' is not followed by whitespace.102
     ErrorwhitespaceWhitespaceAround'=' is not preceded with whitespace.102
     ErrorwhitespaceWhitespaceAround'=' is not followed by whitespace.102
     ErrorwhitespaceWhitespaceAround'=' is not preceded with whitespace.102
     ErrorwhitespaceWhitespaceAround'=' is not followed by whitespace.102
     ErrorwhitespaceWhitespaceAround'=' is not preceded with whitespace.102
     ErrorwhitespaceWhitespaceAround'=' is not followed by whitespace.104
     ErrorwhitespaceWhitespaceAround'=' is not preceded with whitespace.104
     ErrorsizesLineLengthLine is longer than 80 characters (found 82).106
     ErrorsizesLineLengthLine is longer than 80 characters (found 498).107
     ErrorwhitespaceWhitespaceAround'=' is not followed by whitespace.107
     ErrorwhitespaceWhitespaceAround'=' is not preceded with whitespace.107
     ErrorwhitespaceWhitespaceAround'=' is not followed by whitespace.107
     ErrorwhitespaceWhitespaceAround'=' is not preceded with whitespace.107
     ErrorwhitespaceWhitespaceAround'=' is not followed by whitespace.107
     ErrorwhitespaceWhitespaceAround'=' is not preceded with whitespace.107
     ErrorcodingMagicNumber'300' is a magic number.107
     ErrorwhitespaceWhitespaceAfter',' is not followed by whitespace.107
     ErrorcodingMagicNumber'24' is a magic number.107
     ErrorsizesLineLengthLine is longer than 80 characters (found 459).108
     ErrorwhitespaceWhitespaceAround'=' is not followed by whitespace.108
     ErrorwhitespaceWhitespaceAround'=' is not preceded with whitespace.108
     ErrorwhitespaceWhitespaceAround'=' is not followed by whitespace.108
     ErrorwhitespaceWhitespaceAround'=' is not preceded with whitespace.108
     ErrorwhitespaceWhitespaceAround'=' is not followed by whitespace.108
     ErrorwhitespaceWhitespaceAround'=' is not preceded with whitespace.108
     ErrorcodingMagicNumber'300' is a magic number.108
     ErrorwhitespaceWhitespaceAfter',' is not followed by whitespace.108
     ErrorcodingMagicNumber'24' is a magic number.108
     ErrorsizesLineLengthLine is longer than 80 characters (found 470).109
     ErrorwhitespaceWhitespaceAround'=' is not followed by whitespace.109
     ErrorwhitespaceWhitespaceAround'=' is not preceded with whitespace.109
     ErrorcodingMagicNumber'3' is a magic number.109
     ErrorwhitespaceWhitespaceAround'=' is not followed by whitespace.109
     ErrorwhitespaceWhitespaceAround'=' is not preceded with whitespace.109
     ErrorwhitespaceWhitespaceAround'=' is not followed by whitespace.109
     ErrorwhitespaceWhitespaceAround'=' is not preceded with whitespace.109
     ErrorcodingMagicNumber'100' is a magic number.109
     ErrorwhitespaceWhitespaceAfter',' is not followed by whitespace.109
     ErrorcodingMagicNumber'24' is a magic number.109
     ErrorsizesLineLengthLine is longer than 80 characters (found 508).110
     ErrorwhitespaceWhitespaceAround'=' is not followed by whitespace.110
     ErrorwhitespaceWhitespaceAround'=' is not preceded with whitespace.110
     ErrorcodingMagicNumber'4' is a magic number.110
     ErrorwhitespaceWhitespaceAround'=' is not followed by whitespace.110
     ErrorwhitespaceWhitespaceAround'=' is not preceded with whitespace.110
     ErrorwhitespaceWhitespaceAround'=' is not followed by whitespace.110
     ErrorwhitespaceWhitespaceAround'=' is not preceded with whitespace.110
     ErrorcodingMagicNumber'100' is a magic number.110
     ErrorwhitespaceWhitespaceAfter',' is not followed by whitespace.110
     ErrorcodingMagicNumber'24' is a magic number.110
     ErrorwhitespaceWhitespaceAround'=' is not followed by whitespace.112
     ErrorwhitespaceWhitespaceAround'=' is not preceded with whitespace.112
     ErrorcodingMagicNumber'5' is a magic number.112
     ErrorwhitespaceWhitespaceAround'=' is not followed by whitespace.112
     ErrorwhitespaceWhitespaceAround'=' is not preceded with whitespace.112
     ErrorwhitespaceWhitespaceAround'=' is not followed by whitespace.112
     ErrorwhitespaceWhitespaceAround'=' is not preceded with whitespace.112
     ErrorcodingMagicNumber'32' is a magic number.114
     ErrorsizesLineLengthLine is longer than 80 characters (found 82).115
     ErrorwhitespaceWhitespaceAround'->' is not preceded with whitespace.115
     ErrorblocksLeftCurly'{' at column 48 should have line break after.115
     ErrorsizesLineLengthLine is longer than 80 characters (found 83).121
     ErrorsizesLineLengthLine is longer than 80 characters (found 115).126
     ErrorwhitespaceWhitespaceAround'->' is not followed by whitespace.126
     ErrorwhitespaceWhitespaceAround'->' is not preceded with whitespace.126
     ErrorblocksLeftCurly'{' at column 36 should have line break after.126
     ErrorwhitespaceWhitespaceAround'{' is not preceded with whitespace.126
     ErrorsizesLineLengthLine is longer than 80 characters (found 86).138
     ErrorsizesLineLengthLine is longer than 80 characters (found 86).149
     ErrorsizesLineLengthLine is longer than 80 characters (found 155).150
     ErrorsizesLineLengthLine is longer than 80 characters (found 105).155
     ErrorsizesLineLengthLine is longer than 80 characters (found 97).156
     ErrorsizesLineLengthLine is longer than 80 characters (found 118).157
     ErrorsizesLineLengthLine is longer than 80 characters (found 105).164
     ErrorsizesLineLengthLine is longer than 80 characters (found 143).166
     ErrorsizesLineLengthLine is longer than 80 characters (found 111).171
     ErrorsizesLineLengthLine is longer than 80 characters (found 144).173
     ErrorsizesLineLengthLine is longer than 80 characters (found 115).178
     ErrorsizesLineLengthLine is longer than 80 characters (found 81).180
     ErrorsizesLineLengthLine is longer than 80 characters (found 168).181
     ErrorsizesLineLengthLine is longer than 80 characters (found 160).182
     ErrorsizesLineLengthLine is longer than 80 characters (found 96).184
     ErrorsizesLineLengthLine is longer than 80 characters (found 119).187
     ErrorsizesLineLengthLine is longer than 80 characters (found 123).188
     ErrorsizesLineLengthLine is longer than 80 characters (found 100).191
     ErrorsizesLineLengthLine is longer than 80 characters (found 123).192
     ErrorsizesLineLengthLine is longer than 80 characters (found 96).195
     ErrorsizesLineLengthLine is longer than 80 characters (found 159).197
     ErrorsizesLineLengthLine is longer than 80 characters (found 126).198
     ErrorsizesLineLengthLine is longer than 80 characters (found 118).199
     ErrorsizesLineLengthLine is longer than 80 characters (found 94).200
     ErrorsizesLineLengthLine is longer than 80 characters (found 98).201
     ErrorsizesLineLengthLine is longer than 80 characters (found 81).202
     ErrorsizesLineLengthLine is longer than 80 characters (found 112).203
     ErrorsizesLineLengthLine is longer than 80 characters (found 94).207
     ErrorsizesLineLengthLine is longer than 80 characters (found 155).208
     ErrorsizesLineLengthLine is longer than 80 characters (found 234).209
     ErrorsizesLineLengthLine is longer than 80 characters (found 168).211
     ErrorsizesLineLengthLine is longer than 80 characters (found 146).213
     ErrorsizesLineLengthLine is longer than 80 characters (found 138).214
     ErrorsizesLineLengthLine is longer than 80 characters (found 114).215
     ErrorsizesLineLengthLine is longer than 80 characters (found 118).216
     ErrorsizesLineLengthLine is longer than 80 characters (found 89).219
     ErrorsizesLineLengthLine is longer than 80 characters (found 120).220
     ErrorsizesLineLengthLine is longer than 80 characters (found 93).223
     ErrorsizesLineLengthLine is longer than 80 characters (found 82).226
     ErrorsizesLineLengthLine is longer than 80 characters (found 155).230
     ErrordesignDesignForExtensionClass 'Keyboarding' looks like designed for extension (can be subclassed), but the method 'dateChanged' does not have javadoc that explains how to do that safely. If class is not designed for extension consider making the class 'Keyboarding' final or making the method 'dateChanged' static/final/abstract/empty, or adding allowed annotation for the method.242
     ErrormiscFinalParametersParameter newDate should be final.243
     ErrordesignDesignForExtensionClass 'Keyboarding' looks like designed for extension (can be subclassed), but the method 'studentChanged' does not have javadoc that explains how to do that safely. If class is not designed for extension consider making the class 'Keyboarding' final or making the method 'studentChanged' static/final/abstract/empty, or adding allowed annotation for the method.251
     ErrormiscFinalParametersParameter newStudent should be final.252
     ErrorsizesLineLengthLine is longer than 80 characters (found 119).262
    +

    com/studentgui/apppages/Observations.java

    + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
    SeverityCategoryRuleMessageLine
     ErrorsizesLineLengthLine is longer than 80 characters (found 88).25
     ErrorsizesLineLengthLine is longer than 80 characters (found 96).27
     ErrorsizesLineLengthLine is longer than 80 characters (found 97).28
     ErrorsizesLineLengthLine is longer than 80 characters (found 160).33
     ErrorsizesLineLengthLine is longer than 80 characters (found 135).34
     ErrorsizesLineLengthLine is longer than 80 characters (found 123).35
     ErrorsizesLineLengthLine is longer than 80 characters (found 129).41
     ErrorsizesLineLengthLine is longer than 80 characters (found 103).43
     ErrorsizesLineLengthLine is longer than 80 characters (found 125).44
     ErrorsizesLineLengthLine is longer than 80 characters (found 102).47
     ErrorsizesLineLengthLine is longer than 80 characters (found 82).55
     ErrorjavadocJavadocVariableMissing a Javadoc comment.55
     ErrorsizesLineLengthLine is longer than 80 characters (found 86).59
     ErrorsizesLineLengthLine is longer than 80 characters (found 85).68
     ErrormiscFinalParametersParameter studentName should be final.71
     ErrormiscFinalParametersParameter date should be final.71
     ErrorsizesLineLengthLine is longer than 80 characters (found 149).72
     ErrorcodingMagicNumber'20' is a magic number.79
     ErrorwhitespaceWhitespaceAfter',' is not followed by whitespace.79
     ErrorcodingMagicNumber'20' is a magic number.79
     ErrorwhitespaceWhitespaceAfter',' is not followed by whitespace.79
     ErrorcodingMagicNumber'20' is a magic number.79
     ErrorwhitespaceWhitespaceAfter',' is not followed by whitespace.79
     ErrorcodingMagicNumber'20' is a magic number.79
     ErrorsizesLineLengthLine is longer than 80 characters (found 91).81
     ErrorsizesLineLengthLine is longer than 80 characters (found 169).82
     ErrorwhitespaceWhitespaceAround'=' is not followed by whitespace.82
     ErrorwhitespaceWhitespaceAround'=' is not preceded with whitespace.82
     ErrorwhitespaceWhitespaceAfter',' is not followed by whitespace.82
     ErrorwhitespaceWhitespaceAfter',' is not followed by whitespace.82
     ErrorwhitespaceWhitespaceAfter',' is not followed by whitespace.82
     ErrorwhitespaceWhitespaceAfter',' is not followed by whitespace.84
     ErrorcodingMagicNumber'16' is a magic number.84
     ErrorwhitespaceWhitespaceAround'=' is not followed by whitespace.86
     ErrorwhitespaceWhitespaceAround'=' is not preceded with whitespace.86
     ErrorwhitespaceWhitespaceAround'=' is not followed by whitespace.86
     ErrorwhitespaceWhitespaceAround'=' is not preceded with whitespace.86
     ErrorwhitespaceWhitespaceAround'=' is not followed by whitespace.86
     ErrorwhitespaceWhitespaceAround'=' is not preceded with whitespace.86
     ErrorsizesLineLengthLine is longer than 80 characters (found 95).88
     ErrorwhitespaceWhitespaceAround'=' is not followed by whitespace.88
     ErrorwhitespaceWhitespaceAround'=' is not preceded with whitespace.88
     ErrorwhitespaceWhitespaceAround'=' is not followed by whitespace.88
     ErrorwhitespaceWhitespaceAround'=' is not preceded with whitespace.88
     ErrorsizesLineLengthLine is longer than 80 characters (found 293).89
     ErrorwhitespaceWhitespaceAround'=' is not followed by whitespace.89
     ErrorwhitespaceWhitespaceAround'=' is not preceded with whitespace.89
     ErrorwhitespaceWhitespaceAround'=' is not followed by whitespace.89
     ErrorwhitespaceWhitespaceAround'=' is not preceded with whitespace.89
     ErrorcodingMagicNumber'8' is a magic number.89
     ErrorwhitespaceWhitespaceAfter',' is not followed by whitespace.89
     ErrorcodingMagicNumber'40' is a magic number.89
     ErrorsizesLineLengthLine is longer than 80 characters (found 98).94
     ErrorcodingMagicNumber'3' is a magic number.94
     ErrorcodingMagicNumber'4' is a magic number.98
     ErrorwhitespaceWhitespaceAround'->' is not preceded with whitespace.99
     ErrorsizesLineLengthLine is longer than 80 characters (found 90).106
     ErrorsizesLineLengthLine is longer than 80 characters (found 100).110
     ErrorwhitespaceWhitespaceAround'->' is not followed by whitespace.110
     ErrorwhitespaceWhitespaceAround'->' is not preceded with whitespace.110
     ErrorblocksLeftCurly'{' at column 40 should have line break after.110
     ErrorwhitespaceWhitespaceAround'{' is not preceded with whitespace.110
     ErrorsizesLineLengthLine is longer than 80 characters (found 83).117
     ErrorsizesLineLengthLine is longer than 80 characters (found 86).120
     ErrorsizesLineLengthLine is longer than 80 characters (found 151).121
     ErrorsizesLineLengthLine is longer than 80 characters (found 105).126
     ErrorsizesLineLengthLine is longer than 80 characters (found 98).127
     ErrorsizesLineLengthLine is longer than 80 characters (found 118).128
     ErrorsizesLineLengthLine is longer than 80 characters (found 128).130
     ErrorsizesLineLengthLine is longer than 80 characters (found 82).132
     ErrorsizesLineLengthLine is longer than 80 characters (found 130).135
     ErrorsizesLineLengthLine is longer than 80 characters (found 161).136
     ErrorsizesLineLengthLine is longer than 80 characters (found 97).138
     ErrorsizesLineLengthLine is longer than 80 characters (found 151).142
    +

    com/studentgui/apppages/ScreenReader.java

    + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
    SeverityCategoryRuleMessageLine
     ErrorsizesLineLengthLine is longer than 80 characters (found 86).27
     ErrorsizesLineLengthLine is longer than 80 characters (found 101).28
     ErrorsizesLineLengthLine is longer than 80 characters (found 84).33
     ErrorsizesLineLengthLine is longer than 80 characters (found 83).45
     ErrorsizesLineLengthLine is longer than 80 characters (found 84).49
     ErrorsizesLineLengthLine is longer than 80 characters (found 84).53
     ErrorsizesLineLengthLine is longer than 80 characters (found 82).58
     ErrorsizesLineLengthLine is longer than 80 characters (found 82).61
     ErrorsizesLineLengthLine is longer than 80 characters (found 81).64
     ErrorsizesLineLengthLine is longer than 80 characters (found 90).66
     ErrorsizesLineLengthLine is longer than 80 characters (found 83).67
     ErrorsizesLineLengthLine is longer than 80 characters (found 98).68
     ErrorsizesLineLengthLine is longer than 80 characters (found 81).69
     ErrorsizesLineLengthLine is longer than 80 characters (found 85).70
     ErrorsizesLineLengthLine is longer than 80 characters (found 85).71
     ErrorsizesLineLengthLine is longer than 80 characters (found 112).79
     ErrorsizesLineLengthLine is longer than 80 characters (found 115).80
     ErrorsizesLineLengthLine is longer than 80 characters (found 125).81
     ErrorsizesLineLengthLine is longer than 80 characters (found 122).82
     ErrorsizesLineLengthLine is longer than 80 characters (found 107).83
     ErrorsizesLineLengthLine is longer than 80 characters (found 110).84
     ErrorsizesLineLengthLine is longer than 80 characters (found 95).87
     ErrorsizesLineLengthLine is longer than 80 characters (found 112).88
     ErrorsizesLineLengthLine is longer than 80 characters (found 133).95
     ErrorsizesLineLengthLine is longer than 80 characters (found 82).96
     ErrorjavadocJavadocVariableMissing a Javadoc comment.96
     ErrorsizesLineLengthLine is longer than 80 characters (found 81).102
     ErrorsizesLineLengthLine is longer than 80 characters (found 82).105
     ErrorsizesLineLengthLine is longer than 80 characters (found 90).109
     ErrorsizesLineLengthLine is longer than 80 characters (found 89).119
     ErrorsizesLineLengthLine is longer than 80 characters (found 83).123
     ErrormiscFinalParametersParameter studentName should be final.123
     ErrormiscFinalParametersParameter date should be final.123
     ErrormiscFinalParametersParameter lineGraph should be final.123
     ErrorcodingHiddenField'lineGraph' hides a field.123
     ErrorsizesLineLengthLine is longer than 80 characters (found 149).124
     ErrorsizesLineLengthLine is longer than 80 characters (found 186).130
     ErrorwhitespaceWhitespaceAfter',' is not followed by whitespace.130
     ErrorwhitespaceWhitespaceAfter',' is not followed by whitespace.130
     ErrorwhitespaceWhitespaceAfter',' is not followed by whitespace.130
     ErrorwhitespaceWhitespaceAfter',' is not followed by whitespace.130
     ErrorwhitespaceWhitespaceAfter',' is not followed by whitespace.130
     ErrorwhitespaceWhitespaceAfter',' is not followed by whitespace.130
     ErrorwhitespaceWhitespaceAfter',' is not followed by whitespace.130
     ErrorwhitespaceWhitespaceAfter',' is not followed by whitespace.130
     ErrorwhitespaceWhitespaceAfter',' is not followed by whitespace.130
     ErrorwhitespaceWhitespaceAfter',' is not followed by whitespace.130
     ErrorwhitespaceWhitespaceAfter',' is not followed by whitespace.130
     ErrorsizesLineLengthLine is longer than 80 characters (found 103).131
     ErrorwhitespaceWhitespaceAfter',' is not followed by whitespace.131
     ErrorwhitespaceWhitespaceAfter',' is not followed by whitespace.131
     ErrorwhitespaceWhitespaceAfter',' is not followed by whitespace.131
     ErrorwhitespaceWhitespaceAfter',' is not followed by whitespace.131
     ErrorwhitespaceWhitespaceAfter',' is not followed by whitespace.131
     ErrorwhitespaceWhitespaceAfter',' is not followed by whitespace.131
     ErrorwhitespaceWhitespaceAfter',' is not followed by whitespace.131
     ErrorsizesLineLengthLine is longer than 80 characters (found 326).132
     ErrorwhitespaceWhitespaceAfter',' is not followed by whitespace.132
     ErrorwhitespaceWhitespaceAfter',' is not followed by whitespace.132
     ErrorwhitespaceWhitespaceAfter',' is not followed by whitespace.132
     ErrorwhitespaceWhitespaceAfter',' is not followed by whitespace.132
     ErrorwhitespaceWhitespaceAfter',' is not followed by whitespace.132
     ErrorwhitespaceWhitespaceAfter',' is not followed by whitespace.132
     ErrorwhitespaceWhitespaceAfter',' is not followed by whitespace.132
     ErrorwhitespaceWhitespaceAfter',' is not followed by whitespace.132
     ErrorwhitespaceWhitespaceAfter',' is not followed by whitespace.132
     ErrorwhitespaceWhitespaceAfter',' is not followed by whitespace.132
     ErrorwhitespaceWhitespaceAfter',' is not followed by whitespace.132
     ErrorwhitespaceWhitespaceAfter',' is not followed by whitespace.132
     ErrorwhitespaceWhitespaceAfter',' is not followed by whitespace.132
     ErrorwhitespaceWhitespaceAfter',' is not followed by whitespace.132
     ErrorwhitespaceWhitespaceAfter',' is not followed by whitespace.132
     ErrorwhitespaceWhitespaceAfter',' is not followed by whitespace.132
     ErrorwhitespaceWhitespaceAfter',' is not followed by whitespace.132
     ErrorwhitespaceWhitespaceAfter',' is not followed by whitespace.132
     ErrorwhitespaceWhitespaceAfter',' is not followed by whitespace.132
     ErrorwhitespaceWhitespaceAfter',' is not followed by whitespace.132
     ErrorwhitespaceWhitespaceAfter',' is not followed by whitespace.132
     ErrorsizesLineLengthLine is longer than 80 characters (found 200).133
     ErrorwhitespaceWhitespaceAfter',' is not followed by whitespace.133
     ErrorwhitespaceWhitespaceAfter',' is not followed by whitespace.133
     ErrorwhitespaceWhitespaceAfter',' is not followed by whitespace.133
     ErrorwhitespaceWhitespaceAfter',' is not followed by whitespace.133
     ErrorwhitespaceWhitespaceAfter',' is not followed by whitespace.133
     ErrorwhitespaceWhitespaceAfter',' is not followed by whitespace.133
     ErrorwhitespaceWhitespaceAfter',' is not followed by whitespace.133
     ErrorwhitespaceWhitespaceAfter',' is not followed by whitespace.133
     ErrorwhitespaceWhitespaceAfter',' is not followed by whitespace.133
     ErrorwhitespaceWhitespaceAfter',' is not followed by whitespace.133
     ErrorwhitespaceWhitespaceAfter',' is not followed by whitespace.133
     ErrorwhitespaceWhitespaceAfter',' is not followed by whitespace.133
     ErrorwhitespaceWhitespaceAfter',' is not followed by whitespace.133
     ErrorcodingMagicNumber'20' is a magic number.139
     ErrorwhitespaceWhitespaceAfter',' is not followed by whitespace.139
     ErrorcodingMagicNumber'20' is a magic number.139
     ErrorwhitespaceWhitespaceAfter',' is not followed by whitespace.139
     ErrorcodingMagicNumber'20' is a magic number.139
     ErrorwhitespaceWhitespaceAfter',' is not followed by whitespace.139
     ErrorcodingMagicNumber'20' is a magic number.139
     ErrorwhitespaceWhitespaceAfter',' is not followed by whitespace.144
     ErrorwhitespaceWhitespaceAfter',' is not followed by whitespace.144
     ErrorwhitespaceWhitespaceAfter',' is not followed by whitespace.144
     ErrorsizesLineLengthLine is longer than 80 characters (found 103).150
     ErrorsizesLineLengthLine is longer than 80 characters (found 96).152
     ErrorcodingMagicNumber'16' is a magic number.152
     ErrorsizesLineLengthLine is longer than 80 characters (found 83).153
     ErrorsizesLineLengthLine is longer than 80 characters (found 106).156
     ErrorsizesLineLengthLine is longer than 80 characters (found 99).157
     ErrorcodingMagicNumber'12' is a magic number.157
     ErrorsizesLineLengthLine is longer than 80 characters (found 89).158
     ErrorwhitespaceWhitespaceAround'->' is not followed by whitespace.158
     ErrorwhitespaceWhitespaceAround'->' is not preceded with whitespace.158
     ErrorsizesLineLengthLine is longer than 80 characters (found 99).159
     ErrorsizesLineLengthLine is longer than 80 characters (found 104).161
     ErrorcodingMagicNumber'360' is a magic number.161
     ErrorcodingMagicNumber'200' is a magic number.161
     ErrorcodingMagicNumber'50' is a magic number.161
     ErrorsizesLineLengthLine is longer than 80 characters (found 117).167
     ErrorcodingMagicNumber'32' is a magic number.181
     ErrorsizesLineLengthLine is longer than 80 characters (found 83).182
     ErrorblocksLeftCurly'{' at column 49 should have line break after.182
     ErrorsizesLineLengthLine is longer than 80 characters (found 87).184
     ErrorcodingMagicNumber'32' is a magic number.190
     ErrorsizesLineLengthLine is longer than 80 characters (found 97).192
     ErrorsizesLineLengthLine is longer than 80 characters (found 89).193
     ErrorsizesLineLengthLine is longer than 80 characters (found 84).196
     ErrorsizesLineLengthLine is longer than 80 characters (found 102).197
     ErrorsizesLineLengthLine is longer than 80 characters (found 91).200
     ErrorsizesLineLengthLine is longer than 80 characters (found 186).203
     ErrorblocksLeftCurly'{' at column 38 should have line break after.203
     ErrorwhitespaceWhitespaceAfter',' is not followed by whitespace.203
     ErrorsizesLineLengthLine is longer than 80 characters (found 156).208
     ErrorsizesLineLengthLine is longer than 80 characters (found 98).226
     ErrorwhitespaceWhitespaceAfter',' is not followed by whitespace.228
     ErrorwhitespaceWhitespaceAfter',' is not followed by whitespace.228
     ErrorwhitespaceWhitespaceAfter',' is not followed by whitespace.228
     ErrorwhitespaceWhitespaceAfter',' is not followed by whitespace.228
     ErrorwhitespaceWhitespaceAfter',' is not followed by whitespace.228
     ErrorwhitespaceWhitespaceAfter',' is not followed by whitespace.229
     ErrorwhitespaceWhitespaceAfter',' is not followed by whitespace.229
     ErrorwhitespaceWhitespaceAfter',' is not followed by whitespace.229
     ErrorsizesLineLengthLine is longer than 80 characters (found 95).230
     ErrorwhitespaceWhitespaceAfter',' is not followed by whitespace.230
     ErrorwhitespaceWhitespaceAfter',' is not followed by whitespace.230
     ErrorwhitespaceWhitespaceAfter',' is not followed by whitespace.230
     ErrorwhitespaceWhitespaceAfter',' is not followed by whitespace.230
     ErrorwhitespaceWhitespaceAfter',' is not followed by whitespace.230
     ErrorwhitespaceWhitespaceAfter',' is not followed by whitespace.230
     ErrorwhitespaceWhitespaceAfter',' is not followed by whitespace.230
     ErrorwhitespaceWhitespaceAfter',' is not followed by whitespace.230
     ErrorwhitespaceWhitespaceAfter',' is not followed by whitespace.230
     ErrorwhitespaceWhitespaceAfter',' is not followed by whitespace.230
     ErrorwhitespaceWhitespaceAfter',' is not followed by whitespace.231
     ErrorwhitespaceWhitespaceAfter',' is not followed by whitespace.231
     ErrorwhitespaceWhitespaceAfter',' is not followed by whitespace.231
     ErrorwhitespaceWhitespaceAfter',' is not followed by whitespace.231
     ErrorwhitespaceWhitespaceAfter',' is not followed by whitespace.231
     ErrorwhitespaceWhitespaceAfter',' is not followed by whitespace.231
     ErrorsizesLineLengthLine is longer than 80 characters (found 82).233
     ErrorsizesLineLengthLine is longer than 80 characters (found 86).244
     ErrorsizesLineLengthLine is longer than 80 characters (found 160).245
     ErrorsizesLineLengthLine is longer than 80 characters (found 105).249
     ErrorsizesLineLengthLine is longer than 80 characters (found 98).250
     ErrorsizesLineLengthLine is longer than 80 characters (found 118).251
     ErrorsizesLineLengthLine is longer than 80 characters (found 103).258
     ErrorsizesLineLengthLine is longer than 80 characters (found 90).259
     ErrorsizesLineLengthLine is longer than 80 characters (found 82).260
     ErrorsizesLineLengthLine is longer than 80 characters (found 148).261
     ErrorsizesLineLengthLine is longer than 80 characters (found 161).262
     ErrorsizesLineLengthLine is longer than 80 characters (found 101).264
     ErrorsizesLineLengthLine is longer than 80 characters (found 119).267
     ErrorsizesLineLengthLine is longer than 80 characters (found 123).268
     ErrorsizesLineLengthLine is longer than 80 characters (found 100).271
     ErrorsizesLineLengthLine is longer than 80 characters (found 123).272
     ErrorsizesLineLengthLine is longer than 80 characters (found 205).275
     ErrorsizesLineLengthLine is longer than 80 characters (found 94).282
     ErrorsizesLineLengthLine is longer than 80 characters (found 88).283
     ErrorcodingMagicNumber'1000' is a magic number.283
     ErrorcodingMagicNumber'240' is a magic number.283
     ErrorsizesLineLengthLine is longer than 80 characters (found 89).284
     ErrorsizesLineLengthLine is longer than 80 characters (found 99).287
     ErrorsizesLineLengthLine is longer than 80 characters (found 81).288
     ErrorsizesLineLengthLine is longer than 80 characters (found 88).294
     ErrorcodingMagicNumber'1000' is a magic number.294
     ErrorcodingMagicNumber'240' is a magic number.294
     ErrorsizesLineLengthLine is longer than 80 characters (found 159).301
     ErrorsizesLineLengthLine is longer than 80 characters (found 93).302
     ErrorsizesLineLengthLine is longer than 80 characters (found 109).304
     ErrorsizesLineLengthLine is longer than 80 characters (found 81).306
     ErrorsizesLineLengthLine is longer than 80 characters (found 112).307
     ErrorsizesLineLengthLine is longer than 80 characters (found 121).312
     ErrorsizesLineLengthLine is longer than 80 characters (found 100).315
     ErrorsizesLineLengthLine is longer than 80 characters (found 96).316
     ErrorsizesLineLengthLine is longer than 80 characters (found 94).319
     ErrorsizesLineLengthLine is longer than 80 characters (found 155).320
     ErrorsizesLineLengthLine is longer than 80 characters (found 403).321
     ErrorsizesLineLengthLine is longer than 80 characters (found 168).323
     ErrorsizesLineLengthLine is longer than 80 characters (found 98).324
     ErrorsizesLineLengthLine is longer than 80 characters (found 148).328
     ErrorsizesLineLengthLine is longer than 80 characters (found 112).329
     ErrorsizesLineLengthLine is longer than 80 characters (found 86).338
     ErrorsizesLineLengthLine is longer than 80 characters (found 89).348
     ErrorsizesLineLengthLine is longer than 80 characters (found 120).349
     ErrorsizesLineLengthLine is longer than 80 characters (found 84).350
     ErrorsizesLineLengthLine is longer than 80 characters (found 94).352
     ErrorsizesLineLengthLine is longer than 80 characters (found 109).355
     ErrorsizesLineLengthLine is longer than 80 characters (found 110).357
     ErrorsizesLineLengthLine is longer than 80 characters (found 156).363
     ErrorsizesLineLengthLine is longer than 80 characters (found 150).373
     ErrorcodingMagicNumber'5' is a magic number.373
     ErrorsizesLineLengthLine is longer than 80 characters (found 82).382
     ErrorsizesLineLengthLine is longer than 80 characters (found 90).388
     ErrorsizesLineLengthLine is longer than 80 characters (found 115).389
     ErrordesignDesignForExtensionClass 'ScreenReader' looks like designed for extension (can be subclassed), but the method 'dateChanged' does not have javadoc that explains how to do that safely. If class is not designed for extension consider making the class 'ScreenReader' final or making the method 'dateChanged' static/final/abstract/empty, or adding allowed annotation for the method.392
     ErrormiscFinalParametersParameter newDate should be final.393
     ErrordesignDesignForExtensionClass 'ScreenReader' looks like designed for extension (can be subclassed), but the method 'studentChanged' does not have javadoc that explains how to do that safely. If class is not designed for extension consider making the class 'ScreenReader' final or making the method 'studentChanged' static/final/abstract/empty, or adding allowed annotation for the method.401
     ErrormiscFinalParametersParameter newStudent should be final.402
     ErrorsizesLineLengthLine is longer than 80 characters (found 119).412
     ErrorsizesLineLengthLine is longer than 80 characters (found 119).420
     ErrorsizesLineLengthLine is longer than 80 characters (found 96).422
     ErrorblocksLeftCurly'{' at column 9 should have line break after.425
     ErrorblocksRightCurly'}' at column 59 should be on the same line as the next part of a multi-block statement (one that directly contains multiple blocks: if/else-if/else, do/while or try/catch/finally).425
     ErrorsizesLineLengthLine is longer than 80 characters (found 187).426
     ErrorblocksLeftCurly'{' at column 88 should have line break after.426
    +

    com/studentgui/apppages/SessionNotes.java

    + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
    SeverityCategoryRuleMessageLine
     ErrorsizesLineLengthLine is longer than 80 characters (found 87).26
     ErrorsizesLineLengthLine is longer than 80 characters (found 100).27
     ErrorsizesLineLengthLine is longer than 80 characters (found 93).28
     ErrorsizesLineLengthLine is longer than 80 characters (found 122).33
     ErrorsizesLineLengthLine is longer than 80 characters (found 115).34
     ErrorsizesLineLengthLine is longer than 80 characters (found 127).35
     ErrorsizesLineLengthLine is longer than 80 characters (found 133).41
     ErrorsizesLineLengthLine is longer than 80 characters (found 97).42
     ErrorsizesLineLengthLine is longer than 80 characters (found 125).43
     ErrorsizesLineLengthLine is longer than 80 characters (found 94).47
     ErrorsizesLineLengthLine is longer than 80 characters (found 100).48
     ErrorsizesLineLengthLine is longer than 80 characters (found 86).49
     ErrorsizesLineLengthLine is longer than 80 characters (found 82).55
     ErrorjavadocJavadocVariableMissing a Javadoc comment.55
     ErrorsizesLineLengthLine is longer than 80 characters (found 88).59
     ErrorsizesLineLengthLine is longer than 80 characters (found 85).69
     ErrormiscFinalParametersParameter studentName should be final.73
     ErrormiscFinalParametersParameter date should be final.73
     ErrormiscFinalParametersParameter graph should be final.73
     ErrorsizesLineLengthLine is longer than 80 characters (found 149).74
     ErrorcodingMagicNumber'20' is a magic number.81
     ErrorwhitespaceWhitespaceAfter',' is not followed by whitespace.81
     ErrorcodingMagicNumber'20' is a magic number.81
     ErrorwhitespaceWhitespaceAfter',' is not followed by whitespace.81
     ErrorcodingMagicNumber'20' is a magic number.81
     ErrorwhitespaceWhitespaceAfter',' is not followed by whitespace.81
     ErrorcodingMagicNumber'20' is a magic number.81
     ErrorsizesLineLengthLine is longer than 80 characters (found 92).83
     ErrorsizesLineLengthLine is longer than 80 characters (found 169).84
     ErrorwhitespaceWhitespaceAround'=' is not followed by whitespace.84
     ErrorwhitespaceWhitespaceAround'=' is not preceded with whitespace.84
     ErrorwhitespaceWhitespaceAfter',' is not followed by whitespace.84
     ErrorwhitespaceWhitespaceAfter',' is not followed by whitespace.84
     ErrorwhitespaceWhitespaceAfter',' is not followed by whitespace.84
     ErrorcodingMagicNumber'16' is a magic number.86
     ErrorwhitespaceWhitespaceAround'=' is not followed by whitespace.88
     ErrorwhitespaceWhitespaceAround'=' is not preceded with whitespace.88
     ErrorwhitespaceWhitespaceAround'=' is not followed by whitespace.88
     ErrorwhitespaceWhitespaceAround'=' is not preceded with whitespace.88
     ErrorwhitespaceWhitespaceAround'=' is not followed by whitespace.88
     ErrorwhitespaceWhitespaceAround'=' is not preceded with whitespace.88
     ErrorsizesLineLengthLine is longer than 80 characters (found 82).90
     ErrorsizesLineLengthLine is longer than 80 characters (found 199).91
     ErrorwhitespaceWhitespaceAround'=' is not followed by whitespace.91
     ErrorwhitespaceWhitespaceAround'=' is not preceded with whitespace.91
     ErrorwhitespaceWhitespaceAround'=' is not followed by whitespace.91
     ErrorwhitespaceWhitespaceAround'=' is not preceded with whitespace.91
     ErrorsizesLineLengthLine is longer than 80 characters (found 282).92
     ErrorwhitespaceWhitespaceAround'=' is not followed by whitespace.92
     ErrorwhitespaceWhitespaceAround'=' is not preceded with whitespace.92
     ErrorwhitespaceWhitespaceAround'=' is not followed by whitespace.92
     ErrorwhitespaceWhitespaceAround'=' is not preceded with whitespace.92
     ErrorcodingMagicNumber'8' is a magic number.92
     ErrorwhitespaceWhitespaceAfter',' is not followed by whitespace.92
     ErrorcodingMagicNumber'40' is a magic number.92
     ErrorwhitespaceWhitespaceAround'=' is not followed by whitespace.95
     ErrorwhitespaceWhitespaceAround'=' is not preceded with whitespace.95
     ErrorcodingMagicNumber'3' is a magic number.95
     ErrorwhitespaceWhitespaceAround'->' is not preceded with whitespace.96
     ErrorsizesLineLengthLine is longer than 80 characters (found 100).104
     ErrorwhitespaceWhitespaceAround'->' is not followed by whitespace.104
     ErrorwhitespaceWhitespaceAround'->' is not preceded with whitespace.104
     ErrorblocksLeftCurly'{' at column 40 should have line break after.104
     ErrorwhitespaceWhitespaceAround'{' is not preceded with whitespace.104
     ErrorsizesLineLengthLine is longer than 80 characters (found 86).115
     ErrorsizesLineLengthLine is longer than 80 characters (found 176).116
     ErrorsizesLineLengthLine is longer than 80 characters (found 105).121
     ErrorsizesLineLengthLine is longer than 80 characters (found 98).122
     ErrorsizesLineLengthLine is longer than 80 characters (found 118).123
     ErrorsizesLineLengthLine is longer than 80 characters (found 82).125
     ErrorsizesLineLengthLine is longer than 80 characters (found 130).128
     ErrorsizesLineLengthLine is longer than 80 characters (found 161).129
     ErrorsizesLineLengthLine is longer than 80 characters (found 97).131
     ErrorsizesLineLengthLine is longer than 80 characters (found 176).135
    +

    com/studentgui/apptheming/Theme.java

    + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
    SeverityCategoryRuleMessageLine
     ErrorjavadocJavadocPackageMissing package-info.java file.1
     ErrorsizesLineLengthLine is longer than 80 characters (found 88).42
     ErrorsizesLineLengthLine is longer than 80 characters (found 119).43
     ErrorsizesLineLengthLine is longer than 80 characters (found 120).45
     ErrorsizesLineLengthLine is longer than 80 characters (found 84).53
     ErrorsizesLineLengthLine is longer than 80 characters (found 92).55
     ErrorsizesLineLengthLine is longer than 80 characters (found 90).59
     ErrorsizesLineLengthLine is longer than 80 characters (found 81).60
     ErrorsizesLineLengthLine is longer than 80 characters (found 89).64
     ErrorsizesLineLengthLine is longer than 80 characters (found 105).65
     ErrorsizesLineLengthLine is longer than 80 characters (found 101).66
     ErrorsizesLineLengthLine is longer than 80 characters (found 85).67
     ErrorsizesLineLengthLine is longer than 80 characters (found 119).70
     ErrorsizesLineLengthLine is longer than 80 characters (found 101).71
     ErrorsizesLineLengthLine is longer than 80 characters (found 91).74
     ErrorsizesLineLengthLine is longer than 80 characters (found 88).75
     ErrordesignFinalClassClass Theme should be declared as final.82
     ErrorsizesMethodLengthMethod createMenuBar length is 271 lines (max allowed is 150).88
     ErrorsizesLineLengthLine is longer than 80 characters (found 97).95
     ErrorblocksLeftCurly'{' at column 62 should have line break after.95
     ErrorsizesLineLengthLine is longer than 80 characters (found 121).98
     ErrorcodingMagicNumber'0x4A90E2' is a magic number.99
     ErrorcodingMagicNumber'12' is a magic number.99
     ErrorsizesLineLengthLine is longer than 80 characters (found 83).101
     ErrorsizesLineLengthLine is longer than 80 characters (found 105).108
     ErrorblocksLeftCurly'{' at column 72 should have line break after.108
     ErrorsizesLineLengthLine is longer than 80 characters (found 123).110
     ErrorcodingMagicNumber'0xF5A623' is a magic number.111
     ErrorcodingMagicNumber'12' is a magic number.111
     ErrorsizesLineLengthLine is longer than 80 characters (found 94).113
     ErrorsizesLineLengthLine is longer than 80 characters (found 106).117
     ErrorblocksLeftCurly'{' at column 72 should have line break after.117
     ErrorsizesLineLengthLine is longer than 80 characters (found 124).119
     ErrorcodingMagicNumber'0x50E3C2' is a magic number.120
     ErrorcodingMagicNumber'12' is a magic number.120
     ErrorsizesLineLengthLine is longer than 80 characters (found 96).122
     ErrorsizesLineLengthLine is longer than 80 characters (found 87).130
     ErrorsizesLineLengthLine is longer than 80 characters (found 110).131
     ErrorblocksLeftCurly'{' at column 72 should have line break after.131
     ErrorsizesLineLengthLine is longer than 80 characters (found 128).133
     ErrorcodingMagicNumber'0x7B61FF' is a magic number.134
     ErrorcodingMagicNumber'12' is a magic number.134
     ErrorsizesLineLengthLine is longer than 80 characters (found 82).135
     ErrorsizesLineLengthLine is longer than 80 characters (found 103).136
     ErrorsizesLineLengthLine is longer than 80 characters (found 84).139
     ErrorsizesLineLengthLine is longer than 80 characters (found 111).140
     ErrorblocksLeftCurly'{' at column 72 should have line break after.140
     ErrorsizesLineLengthLine is longer than 80 characters (found 129).142
     ErrorcodingMagicNumber'0xF8E71C' is a magic number.143
     ErrorcodingMagicNumber'12' is a magic number.143
     ErrorsizesLineLengthLine is longer than 80 characters (found 100).145
     ErrorsizesLineLengthLine is longer than 80 characters (found 114).149
     ErrorblocksLeftCurly'{' at column 72 should have line break after.149
     ErrorsizesLineLengthLine is longer than 80 characters (found 119).151
     ErrorcodingMagicNumber'0x7ED321' is a magic number.152
     ErrorcodingMagicNumber'12' is a magic number.152
     ErrorsizesLineLengthLine is longer than 80 characters (found 93).154
     ErrorsizesLineLengthLine is longer than 80 characters (found 102).158
     ErrorblocksLeftCurly'{' at column 72 should have line break after.158
     ErrorsizesLineLengthLine is longer than 80 characters (found 120).160
     ErrorcodingMagicNumber'0x00A5E0' is a magic number.161
     ErrorcodingMagicNumber'12' is a magic number.161
     ErrorsizesLineLengthLine is longer than 80 characters (found 95).163
     ErrorsizesLineLengthLine is longer than 80 characters (found 81).166
     ErrorsizesLineLengthLine is longer than 80 characters (found 110).167
     ErrorblocksLeftCurly'{' at column 72 should have line break after.167
     ErrorsizesLineLengthLine is longer than 80 characters (found 128).169
     ErrorcodingMagicNumber'0x8B572A' is a magic number.170
     ErrorcodingMagicNumber'12' is a magic number.170
     ErrorsizesLineLengthLine is longer than 80 characters (found 104).172
     ErrorsizesLineLengthLine is longer than 80 characters (found 84).175
     ErrorsizesLineLengthLine is longer than 80 characters (found 111).176
     ErrorblocksLeftCurly'{' at column 72 should have line break after.176
     ErrorsizesLineLengthLine is longer than 80 characters (found 129).178
     ErrorcodingMagicNumber'0x417505' is a magic number.179
     ErrorcodingMagicNumber'12' is a magic number.179
     ErrorsizesLineLengthLine is longer than 80 characters (found 100).181
     ErrorsizesLineLengthLine is longer than 80 characters (found 109).190
     ErrorblocksLeftCurly'{' at column 72 should have line break after.190
     ErrorsizesLineLengthLine is longer than 80 characters (found 127).192
     ErrorcodingMagicNumber'0xF18805' is a magic number.193
     ErrorcodingMagicNumber'12' is a magic number.193
     ErrorsizesLineLengthLine is longer than 80 characters (found 96).195
     ErrorsizesLineLengthLine is longer than 80 characters (found 83).198
     ErrorsizesLineLengthLine is longer than 80 characters (found 111).199
     ErrorblocksLeftCurly'{' at column 72 should have line break after.199
     ErrorsizesLineLengthLine is longer than 80 characters (found 129).201
     ErrorcodingMagicNumber'0x50E3C2' is a magic number.202
     ErrorcodingMagicNumber'12' is a magic number.202
     ErrorsizesLineLengthLine is longer than 80 characters (found 99).204
     ErrorsizesLineLengthLine is longer than 80 characters (found 84).207
     ErrorsizesLineLengthLine is longer than 80 characters (found 111).208
     ErrorblocksLeftCurly'{' at column 72 should have line break after.208
     ErrorsizesLineLengthLine is longer than 80 characters (found 129).210
     ErrorcodingMagicNumber'0xD0021B' is a magic number.211
     ErrorcodingMagicNumber'12' is a magic number.211
     ErrorsizesLineLengthLine is longer than 80 characters (found 100).213
     ErrorsizesLineLengthLine is longer than 80 characters (found 87).223
     ErrorsizesLineLengthLine is longer than 80 characters (found 115).227
     ErrorsizesLineLengthLine is longer than 80 characters (found 156).228
     ErrorblocksLeftCurly'{' at column 72 should have line break after.228
     ErrorcodingMagicNumber'12' is a magic number.230
     ErrorsizesLineLengthLine is longer than 80 characters (found 91).232
     ErrorsizesLineLengthLine is longer than 80 characters (found 102).233
     ErrorsizesLineLengthLine is longer than 80 characters (found 113).239
     ErrorsizesLineLengthLine is longer than 80 characters (found 154).240
     ErrorblocksLeftCurly'{' at column 72 should have line break after.240
     ErrorcodingMagicNumber'0x2C2C2C' is a magic number.242
     ErrorcodingMagicNumber'12' is a magic number.242
     ErrorsizesLineLengthLine is longer than 80 characters (found 89).244
     ErrorsizesLineLengthLine is longer than 80 characters (found 100).245
     ErrorsizesLineLengthLine is longer than 80 characters (found 131).251
     ErrorsizesLineLengthLine is longer than 80 characters (found 160).252
     ErrorblocksLeftCurly'{' at column 72 should have line break after.252
     ErrorcodingMagicNumber'0x4A4A4A' is a magic number.254
     ErrorcodingMagicNumber'12' is a magic number.254
     ErrorsizesLineLengthLine is longer than 80 characters (found 105).256
     ErrorsizesLineLengthLine is longer than 80 characters (found 85).264
     ErrorsizesLineLengthLine is longer than 80 characters (found 96).265
     ErrorsizesLineLengthLine is longer than 80 characters (found 97).266
     ErrorsizesLineLengthLine is longer than 80 characters (found 92).271
     ErrorsizesLineLengthLine is longer than 80 characters (found 168).272
     ErrorblocksLeftCurly'{' at column 80 should have line break after.272
     ErrorcodingMagicNumber'0x888888' is a magic number.274
     ErrorcodingMagicNumber'10' is a magic number.274
     ErrorsizesLineLengthLine is longer than 80 characters (found 89).276
     ErrorsizesLineLengthLine is longer than 80 characters (found 121).282
     ErrorsizesLineLengthLine is longer than 80 characters (found 97).284
     ErrorsizesLineLengthLine is longer than 80 characters (found 84).287
     ErrorsizesLineLengthLine is longer than 80 characters (found 164).288
     ErrorblocksLeftCurly'{' at column 76 should have line break after.288
     ErrorcodingMagicNumber'0x666666' is a magic number.290
     ErrorcodingMagicNumber'10' is a magic number.290
     ErrorsizesLineLengthLine is longer than 80 characters (found 85).292
     ErrorsizesLineLengthLine is longer than 80 characters (found 98).297
     ErrorsizesLineLengthLine is longer than 80 characters (found 113).300
     ErrorsizesLineLengthLine is longer than 80 characters (found 130).301
     ErrorsizesLineLengthLine is longer than 80 characters (found 122).302
     ErrorsizesLineLengthLine is longer than 80 characters (found 139).303
     ErrorsizesLineLengthLine is longer than 80 characters (found 124).304
     ErrorsizesLineLengthLine is longer than 80 characters (found 141).305
     ErrorsizesLineLengthLine is longer than 80 characters (found 112).306
     ErrorsizesLineLengthLine is longer than 80 characters (found 129).307
     ErrorsizesLineLengthLine is longer than 80 characters (found 110).308
     ErrorsizesLineLengthLine is longer than 80 characters (found 127).309
     ErrorsizesLineLengthLine is longer than 80 characters (found 119).310
     ErrorsizesLineLengthLine is longer than 80 characters (found 136).311
     ErrorsizesLineLengthLine is longer than 80 characters (found 115).312
     ErrorsizesLineLengthLine is longer than 80 characters (found 132).313
     ErrorsizesLineLengthLine is longer than 80 characters (found 127).314
     ErrorsizesLineLengthLine is longer than 80 characters (found 144).315
     ErrorsizesLineLengthLine is longer than 80 characters (found 134).316
     ErrorsizesLineLengthLine is longer than 80 characters (found 151).317
     ErrorsizesLineLengthLine is longer than 80 characters (found 129).318
     ErrorsizesLineLengthLine is longer than 80 characters (found 146).319
     ErrorsizesLineLengthLine is longer than 80 characters (found 129).320
     ErrorsizesLineLengthLine is longer than 80 characters (found 146).321
     ErrorsizesLineLengthLine is longer than 80 characters (found 133).322
     ErrorsizesLineLengthLine is longer than 80 characters (found 150).323
     ErrorsizesLineLengthLine is longer than 80 characters (found 119).324
     ErrorsizesLineLengthLine is longer than 80 characters (found 136).325
     ErrorsizesLineLengthLine is longer than 80 characters (found 116).326
     ErrorsizesLineLengthLine is longer than 80 characters (found 133).327
     ErrorsizesLineLengthLine is longer than 80 characters (found 115).328
     ErrorsizesLineLengthLine is longer than 80 characters (found 132).329
     ErrorsizesLineLengthLine is longer than 80 characters (found 125).330
     ErrorsizesLineLengthLine is longer than 80 characters (found 142).331
     ErrorsizesLineLengthLine is longer than 80 characters (found 127).332
     ErrorsizesLineLengthLine is longer than 80 characters (found 143).333
     ErrorsizesLineLengthLine is longer than 80 characters (found 168).341
     ErrorblocksLeftCurly'{' at column 80 should have line break after.341
     ErrorcodingMagicNumber'0x666666' is a magic number.343
     ErrorcodingMagicNumber'10' is a magic number.343
     ErrorsizesLineLengthLine is longer than 80 characters (found 89).345
     ErrorjavadocJavadocMethod@return tag should be present and have description.371
     ErrorjavadocJavadocMethodExpected @param tag for 'color'.371
     ErrorjavadocJavadocMethodExpected @param tag for 'size'.371
     ErrorsizesLineLengthLine is longer than 80 characters (found 87).372
     ErrorsizesLineLengthLine is longer than 80 characters (found 99).375
     ErrorsizesLineLengthLine is longer than 80 characters (found 88).377
     ErrorwhitespaceWhitespaceAround'/' is not followed by whitespace.377
     ErrorwhitespaceWhitespaceAround'/' is not preceded with whitespace.377
     ErrorcodingMagicNumber'4' is a magic number.377
     ErrorwhitespaceWhitespaceAround'/' is not followed by whitespace.377
     ErrorwhitespaceWhitespaceAround'/' is not preceded with whitespace.377
     ErrorcodingMagicNumber'4' is a magic number.377
     ErrorsizesLineLengthLine is longer than 80 characters (found 85).390
     ErrorsizesLineLengthLine is longer than 80 characters (found 104).408
     ErrorsizesLineLengthLine is longer than 80 characters (found 96).409
     ErrorsizesLineLengthLine is longer than 80 characters (found 87).422
     ErrorsizesLineLengthLine is longer than 80 characters (found 113).423
    +

    com/studentgui/bootstrap/Bootstrap.java

    + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
    SeverityCategoryRuleMessageLine
     ErrorjavadocJavadocPackageMissing package-info.java file.1
     ErrorblocksLeftCurly'{' at column 25 should have line break after.10
     ErrorjavadocMissingJavadocMethodMissing a Javadoc comment.12
     ErrorsizesLineLengthLine is longer than 80 characters (found 83).14
     ErrorsizesLineLengthLine is longer than 80 characters (found 84).17
     ErrorsizesLineLengthLine is longer than 80 characters (found 150).24
     ErrorsizesLineLengthLine is longer than 80 characters (found 99).28
     ErrorsizesLineLengthLine is longer than 80 characters (found 81).31
     ErrorsizesLineLengthLine is longer than 80 characters (found 110).33
    +

    com/studentgui/test/BrailleSmokeTest.java

    + + + + + + + + + + + + + + + + + + +
    SeverityCategoryRuleMessageLine
     ErrorjavadocJavadocPackageMissing package-info.java file.1
     ErrorsizesLineLengthLine is longer than 80 characters (found 82).8
    +

    com/studentgui/tools/GroupedSmoke.java

    + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
    SeverityCategoryRuleMessageLine
     ErrorsizesLineLengthLine is longer than 80 characters (found 87).15
     ErrorsizesLineLengthLine is longer than 80 characters (found 87).16
     ErrorsizesLineLengthLine is longer than 80 characters (found 88).21
     ErrorsizesLineLengthLine is longer than 80 characters (found 115).34
     ErrorsizesLineLengthLine is longer than 80 characters (found 96).40
     ErrorsizesLineLengthLine is longer than 80 characters (found 97).41
     ErrorsizesLineLengthLine is longer than 80 characters (found 108).42
     ErrorsizesLineLengthLine is longer than 80 characters (found 115).45
     ErrorsizesLineLengthLine is longer than 80 characters (found 96).49
     ErrordesignHideUtilityClassConstructorUtility classes should not have a public or default constructor.57
     ErrorsizesLineLengthLine is longer than 80 characters (found 102).69
     ErrorwhitespaceWhitespaceAfter',' is not followed by whitespace.69
     ErrorwhitespaceWhitespaceAfter',' is not followed by whitespace.69
     ErrorwhitespaceWhitespaceAfter',' is not followed by whitespace.69
     ErrorwhitespaceWhitespaceAfter',' is not followed by whitespace.69
     ErrorwhitespaceWhitespaceAfter',' is not followed by whitespace.69
     ErrorwhitespaceWhitespaceAfter',' is not followed by whitespace.69
     ErrorwhitespaceWhitespaceAfter',' is not followed by whitespace.69
     ErrorwhitespaceWhitespaceAfter',' is not followed by whitespace.69
     ErrorcodingMagicNumber'3' is a magic number.73
     ErrorcodingMagicNumber'5' is a magic number.76
     ErrorsizesLineLengthLine is longer than 80 characters (found 127).82
     ErrorsizesLineLengthLine is longer than 80 characters (found 93).85
     ErrorcodingMagicNumber'800' is a magic number.86
     ErrorcodingMagicNumber'600' is a magic number.86
     ErrorsizesLineLengthLine is longer than 80 characters (found 88).87
     ErrorsizesLineLengthLine is longer than 80 characters (found 81).92
    +

    com/studentgui/tools/ProgrammaticPageSaveTest.java

    + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
    SeverityCategoryRuleMessageLine
     ErrorsizesLineLengthLine is longer than 80 characters (found 89).14
     ErrorsizesLineLengthLine is longer than 80 characters (found 81).16
     ErrorsizesLineLengthLine is longer than 80 characters (found 87).18
     ErrorsizesLineLengthLine is longer than 80 characters (found 97).19
     ErrorsizesLineLengthLine is longer than 80 characters (found 89).21
     ErrorsizesLineLengthLine is longer than 80 characters (found 83).26
     ErrorsizesLineLengthLine is longer than 80 characters (found 95).27
     ErrorsizesLineLengthLine is longer than 80 characters (found 85).28
     ErrorsizesLineLengthLine is longer than 80 characters (found 83).29
     ErrorsizesLineLengthLine is longer than 80 characters (found 90).39
     ErrorsizesLineLengthLine is longer than 80 characters (found 90).40
     ErrorsizesLineLengthLine is longer than 80 characters (found 91).41
     ErrorsizesLineLengthLine is longer than 80 characters (found 97).42
     ErrorsizesLineLengthLine is longer than 80 characters (found 92).45
     ErrorsizesLineLengthLine is longer than 80 characters (found 90).46
     ErrorsizesLineLengthLine is longer than 80 characters (found 134).51
     ErrorsizesLineLengthLine is longer than 80 characters (found 97).52
     ErrorsizesLineLengthLine is longer than 80 characters (found 89).53
     ErrorsizesLineLengthLine is longer than 80 characters (found 85).56
     ErrordesignFinalClassClass ProgrammaticPageSaveTest should be declared as final.63
     ErrorsizesLineLengthLine is longer than 80 characters (found 122).77
     ErrorsizesLineLengthLine is longer than 80 characters (found 81).81
     ErrorsizesLineLengthLine is longer than 80 characters (found 91).87
     ErrorsizesLineLengthLine is longer than 80 characters (found 86).89
     ErrorsizesLineLengthLine is longer than 80 characters (found 106).93
     ErrorcodingMagicNumber'3' is a magic number.94
     ErrorsizesLineLengthLine is longer than 80 characters (found 89).105
     ErrorsizesLineLengthLine is longer than 80 characters (found 102).108
     ErrorsizesLineLengthLine is longer than 80 characters (found 88).117
    +

    com/studentgui/tools/QueryStudentData.java

    + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
    SeverityCategoryRuleMessageLine
     ErrorsizesLineLengthLine is longer than 80 characters (found 92).13
     ErrorsizesLineLengthLine is longer than 80 characters (found 83).15
     ErrorsizesLineLengthLine is longer than 80 characters (found 82).19
     ErrorsizesLineLengthLine is longer than 80 characters (found 82).21
     ErrorsizesLineLengthLine is longer than 80 characters (found 88).30
     ErrorsizesLineLengthLine is longer than 80 characters (found 93).47
     ErrorsizesLineLengthLine is longer than 80 characters (found 96).48
     ErrordesignHideUtilityClassConstructorUtility classes should not have a public or default constructor.54
     ErrorsizesLineLengthLine is longer than 80 characters (found 109).83
     ErrorsizesLineLengthLine is longer than 80 characters (found 98).84
     ErrorsizesLineLengthLine is longer than 80 characters (found 140).91
     ErrorsizesLineLengthLine is longer than 80 characters (found 109).99
     ErrorcodingMagicNumber'5' is a magic number.99
     ErrorsizesLineLengthLine is longer than 80 characters (found 146).100
     ErrorsizesLineLengthLine is longer than 80 characters (found 119).102
    +

    com/studentgui/tools/RenderStudentProgress.java

    + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
    SeverityCategoryRuleMessageLine
     ErrorsizesLineLengthLine is longer than 80 characters (found 88).20
     ErrorsizesLineLengthLine is longer than 80 characters (found 103).31
     ErrorsizesLineLengthLine is longer than 80 characters (found 93).36
     ErrorsizesLineLengthLine is longer than 80 characters (found 98).37
     ErrorsizesLineLengthLine is longer than 80 characters (found 111).38
     ErrorsizesLineLengthLine is longer than 80 characters (found 82).39
     ErrorsizesLineLengthLine is longer than 80 characters (found 102).40
     ErrorsizesLineLengthLine is longer than 80 characters (found 88).43
     ErrordesignHideUtilityClassConstructorUtility classes should not have a public or default constructor.50
     ErrorsizesLineLengthLine is longer than 80 characters (found 84).52
     ErrorsizesLineLengthLine is longer than 80 characters (found 82).54
     ErrorsizesLineLengthLine is longer than 80 characters (found 97).59
     ErrorsizesLineLengthLine is longer than 80 characters (found 109).69
     ErrorsizesLineLengthLine is longer than 80 characters (found 188).70
     ErrorblocksNeedBraces'while' construct must use '{}'s.73
     ErrorsizesLineLengthLine is longer than 80 characters (found 89).82
     ErrorcodingMagicNumber'5' is a magic number.82
     ErrorsizesLineLengthLine is longer than 80 characters (found 94).84
     ErrorsizesLineLengthLine is longer than 80 characters (found 116).89
     ErrorsizesLineLengthLine is longer than 80 characters (found 87).92
     ErrorcodingMagicNumber'1000' is a magic number.93
     ErrorcodingMagicNumber'800' is a magic number.93
     ErrorsizesLineLengthLine is longer than 80 characters (found 100).97
    +

    com/studentgui/tools/SmokeTest.java

    + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
    SeverityCategoryRuleMessageLine
     ErrorjavadocJavadocPackageMissing package-info.java file.1
     ErrorsizesLineLengthLine is longer than 80 characters (found 81).13
     ErrorsizesLineLengthLine is longer than 80 characters (found 91).15
     ErrorsizesLineLengthLine is longer than 80 characters (found 84).19
     ErrorsizesLineLengthLine is longer than 80 characters (found 83).21
     ErrorsizesLineLengthLine is longer than 80 characters (found 106).31
     ErrorsizesLineLengthLine is longer than 80 characters (found 81).35
     ErrorsizesLineLengthLine is longer than 80 characters (found 109).39
     ErrorsizesLineLengthLine is longer than 80 characters (found 82).41
     ErrorsizesLineLengthLine is longer than 80 characters (found 86).42
     ErrordesignFinalClassClass SmokeTest should be declared as final.49
     ErrorcodingMagicNumber'3' is a magic number.62
     ErrorcodingMagicNumber'28' is a magic number.64
     ErrorcodingMagicNumber'5' is a magic number.65
     ErrorsizesLineLengthLine is longer than 80 characters (found 124).71
     ErrorsizesLineLengthLine is longer than 80 characters (found 90).73
     ErrorcodingMagicNumber'800' is a magic number.74
     ErrorcodingMagicNumber'400' is a magic number.74
     ErrorsizesLineLengthLine is longer than 80 characters (found 85).75
    +

    com/studentgui/uicomp/PhaseScoreField.java

    + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
    SeverityCategoryRuleMessageLine
     ErrorjavadocJavadocPackageMissing package-info.java file.1
     ErrorsizesLineLengthLine is longer than 80 characters (found 105).24
     ErrorjavadocJavadocVariableMissing a Javadoc comment.24
     ErrorsizesLineLengthLine is longer than 80 characters (found 97).25
     ErrorsizesLineLengthLine is longer than 80 characters (found 141).26
     ErrorjavadocJavadocVariableMissing a Javadoc comment.26
     ErrorjavadocJavadocVariableMissing a Javadoc comment.35
     ErrorcodingMagicNumber'200' is a magic number.35
     ErrorsizesLineLengthLine is longer than 80 characters (found 86).37
     ErrorsizesLineLengthLine is longer than 80 characters (found 81).41
     ErrorcodingMagicNumber'12' is a magic number.55
     ErrorsizesLineLengthLine is longer than 80 characters (found 81).57
     ErrorcodingMagicNumber'40' is a magic number.62
     ErrorcodingMagicNumber'50' is a magic number.62
     ErrorsizesLineLengthLine is longer than 80 characters (found 87).68
     ErrorcodingMagicNumber'4' is a magic number.71
     ErrorsizesLineLengthLine is longer than 80 characters (found 87).73
     ErrorcodingMagicNumber'12' is a magic number.74
     ErrorsizesLineLengthLine is longer than 80 characters (found 83).76
     ErrorsizesLineLengthLine is longer than 80 characters (found 88).78
     ErrorsizesLineLengthLine is longer than 80 characters (found 92).85
     ErrorsizesLineLengthLine is longer than 80 characters (found 108).88
     ErrorcodingMagicNumber'48' is a magic number.91
     ErrorcodingMagicNumber'20' is a magic number.91
     ErrorcodingMagicNumber'48' is a magic number.92
     ErrorcodingMagicNumber'24' is a magic number.92
     ErrorsizesLineLengthLine is longer than 80 characters (found 83).94
     ErrorcodingMagicNumber'20' is a magic number.94
     ErrorcodingMagicNumber'8' is a magic number.103
     ErrorsizesLineLengthLine is longer than 80 characters (found 86).106
     ErrorsizesLineLengthLine is longer than 80 characters (found 82).113
     ErrorwhitespaceNoWhitespaceAfter'{' is followed by whitespace.113
     ErrorcodingMagicNumber'50' is a magic number.115
     ErrorsizesLineLengthLine is longer than 80 characters (found 101).116
     ErrorsizesLineLengthLine is longer than 80 characters (found 101).127
     ErrorcodingMagicNumber'3' is a magic number.128
     ErrorsizesLineLengthLine is longer than 80 characters (found 95).135
     ErrorcodingMagicNumber'50' is a magic number.139
     ErrorsizesLineLengthLine is longer than 80 characters (found 92).157
     ErrorcodingMagicNumber'80' is a magic number.160
     ErrorsizesLineLengthLine is longer than 80 characters (found 87).163
     ErrorcodingMagicNumber'40' is a magic number.165
     ErrorcodingMagicNumber'6' is a magic number.169
     ErrorcodingMagicNumber'40' is a magic number.170
     ErrorblocksLeftCurly'{' at column 45 should have line break after.178
     ErrorsizesLineLengthLine is longer than 80 characters (found 98).188
     ErrorsizesLineLengthLine is longer than 80 characters (found 94).193
     ErrorblocksLeftCurly'{' at column 45 should have line break after.208
     ErrorblocksLeftCurly'{' at column 30 should have line break after.215
     ErrorsizesLineLengthLine is longer than 80 characters (found 86).223
     ErrorsizesLineLengthLine is longer than 80 characters (found 125).228
     ErrorblocksLeftCurly'{' at column 21 should have line break after.228
     ErrorblocksLeftCurly'{' at column 78 should have line break after.228
     ErrorsizesLineLengthLine is longer than 80 characters (found 128).230
     ErrorblocksLeftCurly'{' at column 67 should have line break after.230
     ErrorsizesLineLengthLine is longer than 80 characters (found 88).239
     ErrorblocksLeftCurly'{' at column 39 should have line break after.239
     ErrorcodingMagicNumber'4' is a magic number.239
     ErrordesignDesignForExtensionClass 'PhaseScoreField' looks like designed for extension (can be subclassed), but the method 'setName' does not have javadoc that explains how to do that safely. If class is not designed for extension consider making the class 'PhaseScoreField' final or making the method 'setName' static/final/abstract/empty, or adding allowed annotation for the method.241
     ErrorsizesLineLengthLine is longer than 80 characters (found 83).247
     ErrorblocksLeftCurly'{' at column 30 should have line break after.253
     ErrorblocksLeftCurly'{' at column 36 should have line break after.260
     ErrorsizesLineLengthLine is longer than 80 characters (found 99).263
    +
    +
    +
    +
    +
    + + + diff --git a/target/site/css/maven-base.css b/target/site/css/maven-base.css new file mode 100644 index 0000000..742a735 --- /dev/null +++ b/target/site/css/maven-base.css @@ -0,0 +1,168 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +body { + margin: 0px; + padding: 0px; +} +table { + padding:0px; + width: 100%; + margin-left: -2px; + margin-right: -2px; +} +acronym { + cursor: help; + border-bottom: 1px dotted #feb; +} +table.bodyTable th, table.bodyTable td { + padding: 2px 4px 2px 4px; + vertical-align: top; +} +div.clear { + clear:both; + visibility: hidden; +} +div.clear hr { + display: none; +} +#bannerLeft, #bannerRight { + font-size: xx-large; + font-weight: bold; +} +#bannerLeft img, #bannerRight img { + margin: 0px; +} +.xleft, #bannerLeft img { + float:left; +} +.xright, #bannerRight { + float:right; +} +#banner { + padding: 0px; +} +#breadcrumbs { + padding: 3px 10px 3px 10px; +} +#leftColumn { + width: 170px; + float:left; + overflow: auto; +} +#bodyColumn { + margin-right: 1.5em; + margin-left: 197px; +} +#legend { + padding: 8px 0 8px 0; +} +#navcolumn { + padding: 8px 4px 0 8px; +} +#navcolumn h5 { + margin: 0; + padding: 0; + font-size: small; +} +#navcolumn ul { + margin: 0; + padding: 0; + font-size: small; +} +#navcolumn li { + list-style-type: none; + background-image: none; + background-repeat: no-repeat; + background-position: 0 0.4em; + padding-left: 16px; + list-style-position: outside; + line-height: 1.2em; + font-size: smaller; +} +#navcolumn li.expanded { + background-image: url(../images/expanded.gif); +} +#navcolumn li.collapsed { + background-image: url(../images/collapsed.gif); +} +#navcolumn li.none { + text-indent: -1em; + margin-left: 1em; +} +#poweredBy { + text-align: center; +} +#navcolumn img { + margin-top: 10px; + margin-bottom: 3px; +} +#poweredBy img { + display:block; + margin: 20px 0 20px 17px; +} +#search img { + margin: 0px; + display: block; +} +#search #q, #search #btnG { + border: 1px solid #999; + margin-bottom:10px; +} +#search form { + margin: 0px; +} +#lastPublished { + font-size: x-small; +} +.navSection { + margin-bottom: 2px; + padding: 8px; +} +.navSectionHead { + font-weight: bold; + font-size: x-small; +} +.section { + padding: 4px; +} +#footer { + padding: 3px 10px 3px 10px; + font-size: x-small; +} +#breadcrumbs { + font-size: x-small; + margin: 0pt; +} +.source { + padding: 12px; + margin: 1em 7px 1em 7px; +} +.source pre { + margin: 0px; + padding: 0px; +} +#navcolumn img.imageLink, .imageLink { + padding-left: 0px; + padding-bottom: 0px; + padding-top: 0px; + padding-right: 2px; + border: 0px; + margin: 0px; +} diff --git a/target/site/css/maven-theme.css b/target/site/css/maven-theme.css new file mode 100644 index 0000000..4e2bdfb --- /dev/null +++ b/target/site/css/maven-theme.css @@ -0,0 +1,161 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +body { + padding: 0px 0px 10px 0px; +} +body, td, select, input, li{ + font-family: Verdana, Helvetica, Arial, sans-serif; + font-size: 13px; +} +code{ + font-family: Courier, monospace; + font-size: 13px; +} +a { + text-decoration: none; +} +a:link { + color:#36a; +} +a:visited { + color:#47a; +} +a:active, a:hover { + color:#69c; +} +#legend li.externalLink { + background: url(../images/external.png) left top no-repeat; + padding-left: 18px; +} +a.externalLink, a.externalLink:link, a.externalLink:visited, a.externalLink:active, a.externalLink:hover { + background: url(../images/external.png) right center no-repeat; + padding-right: 18px; +} +#legend li.newWindow { + background: url(../images/newwindow.png) left top no-repeat; + padding-left: 18px; +} +a.newWindow, a.newWindow:link, a.newWindow:visited, a.newWindow:active, a.newWindow:hover { + background: url(../images/newwindow.png) right center no-repeat; + padding-right: 18px; +} +h2 { + padding: 4px 4px 4px 6px; + border: 1px solid #999; + color: #900; + background-color: #ddd; + font-weight:900; + font-size: x-large; +} +h3 { + padding: 4px 4px 4px 6px; + border: 1px solid #aaa; + color: #900; + background-color: #eee; + font-weight: normal; + font-size: large; +} +h4 { + padding: 4px 4px 4px 6px; + border: 1px solid #bbb; + color: #900; + background-color: #fff; + font-weight: normal; + font-size: large; +} +h5 { + padding: 4px 4px 4px 6px; + color: #900; + font-size: medium; +} +p { + line-height: 1.3em; + font-size: small; +} +#breadcrumbs { + border-top: 1px solid #aaa; + border-bottom: 1px solid #aaa; + background-color: #ccc; +} +#leftColumn { + margin: 10px 0 0 5px; + border: 1px solid #999; + background-color: #eee; + padding-bottom: 3px; /* IE-9 scrollbar-fix */ +} +#navcolumn h5 { + font-size: smaller; + border-bottom: 1px solid #aaaaaa; + padding-top: 2px; + color: #000; +} + +table.bodyTable th { + color: white; + background-color: #bbb; + text-align: left; + font-weight: bold; +} + +table.bodyTable th, table.bodyTable td { + font-size: 1em; +} + +table.bodyTable tr.a { + background-color: #ddd; +} + +table.bodyTable tr.b { + background-color: #eee; +} + +.source { + border: 1px solid #999; +} +dl { + padding: 4px 4px 4px 6px; + border: 1px solid #aaa; + background-color: #ffc; +} +dt { + color: #900; +} +#organizationLogo img, #projectLogo img, #projectLogo span{ + margin: 8px; +} +#banner { + border-bottom: 1px solid #fff; +} +.errormark, .warningmark, .donemark, .infomark { + background: url(../images/icon_error_sml.gif) no-repeat; +} + +.warningmark { + background-image: url(../images/icon_warning_sml.gif); +} + +.donemark { + background-image: url(../images/icon_success_sml.gif); +} + +.infomark { + background-image: url(../images/icon_info_sml.gif); +} + diff --git a/target/site/css/print.css b/target/site/css/print.css new file mode 100644 index 0000000..97be85f --- /dev/null +++ b/target/site/css/print.css @@ -0,0 +1,26 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +#banner, #footer, #leftcol, #breadcrumbs, .docs #toc, .docs .courtesylinks, #leftColumn, #navColumn { + display: none !important; +} +#bodyColumn, body.docs div.docs { + margin: 0 !important; + border: none !important +} diff --git a/target/site/css/site.css b/target/site/css/site.css new file mode 100644 index 0000000..055e7e2 --- /dev/null +++ b/target/site/css/site.css @@ -0,0 +1 @@ +/* You can override this file with your own styles */ \ No newline at end of file diff --git a/target/site/dependencies.html b/target/site/dependencies.html new file mode 100644 index 0000000..c0804d5 --- /dev/null +++ b/target/site/dependencies.html @@ -0,0 +1,638 @@ + + + + + + + + Vision Skills Progression Tracker – Project Dependencies + + + + + + + + +
    + +
    +
    +
    +
    +

    Project Dependencies

    +

    compile

    +

    The following is a list of compile dependencies for this project. These dependencies are required to compile and run the application:

    + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
    GroupIdArtifactIdVersionTypeLicenses
    ch.qos.logbacklogback-classic1.5.7jarEclipse Public License - v 1.0GNU Lesser General Public License
    ch.qos.logbacklogback-core1.5.7jarEclipse Public License - v 1.0GNU Lesser General Public License
    com.fasterxml.jackson.corejackson-databind2.15.2jarThe Apache Software License, Version 2.0
    com.formdevflatlaf3.5.1jarThe Apache License, Version 2.0
    com.formdevflatlaf-intellij-themes3.5.1jarThe Apache License, Version 2.0
    com.itextpdfitextpdf5.5.13.4jarGNU Affero General Public License v3
    org.jfreejfreechart1.5.5jarGNU Lesser General Public Licence
    org.slf4jslf4j-api2.0.16jarMIT License
    org.xerialsqlite-jdbc3.46.0.1jarThe Apache Software License, Version 2.0
    +

    test

    +

    The following is a list of test dependencies for this project. These dependencies are only required to compile and run unit tests for the application:

    + + + + + + + + + + + + +
    GroupIdArtifactIdVersionTypeLicenses
    org.junit.jupiterjunit-jupiter5.10.0jarEclipse Public License v2.0
    +

    Project Transitive Dependencies

    +

    The following is a list of transitive dependencies for this project. Transitive dependencies are the dependencies of the project dependencies.

    +

    compile

    +

    The following is a list of compile dependencies for this project. These dependencies are required to compile and run the application:

    + + + + + + + + + + + + + + + + + + +
    GroupIdArtifactIdVersionTypeLicenses
    com.fasterxml.jackson.corejackson-annotations2.15.2jarThe Apache Software License, Version 2.0
    com.fasterxml.jackson.corejackson-core2.15.2jarThe Apache Software License, Version 2.0
    +

    test

    +

    The following is a list of test dependencies for this project. These dependencies are only required to compile and run unit tests for the application:

    + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
    GroupIdArtifactIdVersionTypeLicenses
    org.apiguardianapiguardian-api1.1.2jarThe Apache License, Version 2.0
    org.junit.jupiterjunit-jupiter-api5.10.0jarEclipse Public License v2.0
    org.junit.jupiterjunit-jupiter-engine5.10.0jarEclipse Public License v2.0
    org.junit.jupiterjunit-jupiter-params5.10.0jarEclipse Public License v2.0
    org.junit.platformjunit-platform-commons1.10.0jarEclipse Public License v2.0
    org.junit.platformjunit-platform-engine1.10.0jarEclipse Public License v2.0
    org.opentest4jopentest4j1.3.0jarThe Apache License, Version 2.0
    +

    Project Dependency Graph

    +

    Dependency Tree

    +
    +

    Licenses

    +

    The Apache License, Version 2.0: FlatLaf, FlatLaf IntelliJ Themes Pack, org.apiguardian:apiguardian-api, org.opentest4j:opentest4j

    +

    MIT License: SLF4J API Module

    +

    Apache-2.0: Vision Skills Progression Tracker

    +

    Eclipse Public License v2.0: JUnit Jupiter (Aggregator), JUnit Jupiter API, JUnit Jupiter Engine, JUnit Jupiter Params, JUnit Platform Commons, JUnit Platform Engine API

    +

    GNU Affero General Public License v3: iText Core

    +

    GNU Lesser General Public Licence: JFreeChart

    +

    GNU Lesser General Public License: Logback Classic Module, Logback Core Module

    +

    The Apache Software License, Version 2.0: Jackson-annotations, Jackson-core, SQLite JDBC, jackson-databind

    +

    Eclipse Public License - v 1.0: Logback Classic Module, Logback Core Module

    +

    Dependency File Details

    + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
    FilenameSizeEntriesClassesPackagesJava VersionDebug Information
    logback-classic-1.5.7.jar296.3 kB2371842911Yes
    logback-core-1.5.7.jar615 kB5264644221Yes
    jackson-annotations-2.15.2.jar75.6 kB867329Yes
    jackson-core-2.15.2.jar549.2 kB2501861519Yes
    jackson-databind-2.15.2.jar1.6 MB816777249Yes
    flatlaf-3.5.1.jar904.1 kB37333289Yes
    flatlaf-intellij-themes-3.5.1.jar291.1 kB1345339Yes
    itextpdf-5.5.13.4.jar2.3 MB1008933411.5Yes
    apiguardian-api-1.1.2.jar6.8 kB9329Yes
    jfreechart-1.5.5.jar1.7 MB787692411.8Yes
    junit-jupiter-5.10.0.jar6.4 kB5119No
    junit-jupiter-api-5.10.0.jar211 kB19718289Yes
    junit-jupiter-engine-5.10.0.jar244 kB14713099Yes
    junit-jupiter-params-5.10.0.jar586 kB381347229Yes
    junit-platform-commons-1.10.0.jar106.2 kB644479Yes
    junit-platform-engine-1.10.0.jar204.8 kB153136109Yes
    opentest4j-1.3.0.jar14.3 kB15929Yes
    slf4j-api-2.0.16.jar69.4 kB715659Yes
    sqlite-jdbc-3.46.0.1.jar14.1 MB21012599Yes
    TotalSizeEntriesClassesPackagesJava VersionDebug Information
    1923.9 MB546947272802118
    compile: 11compile: 22.5 MBcompile: 4498compile: 3875compile: 21921compile: 11
    test: 8test: 1.4 MBtest: 971test: 852test: 619test: 7
    +
    +
    +
    +
    +
    + + + diff --git a/target/site/dependency-info.html b/target/site/dependency-info.html new file mode 100644 index 0000000..cf18b83 --- /dev/null +++ b/target/site/dependency-info.html @@ -0,0 +1,100 @@ + + + + + + + + Vision Skills Progression Tracker – Dependency Information + + + + + + + + +
    + +
    +
    +
    +
    +

    Dependency Information

    +

    Apache Maven

    +
    +
    <dependency>
    +  <groupId>com.example</groupId>
    +  <artifactId>Main</artifactId>
    +  <version>1.0.0-beta</version>
    +</dependency>
    +

    Apache Ivy

    +
    +
    <dependency org="com.example" name="Main" rev="1.0.0-beta">
    +  <artifact name="Main" type="jar" />
    +</dependency>
    +

    Groovy Grape

    +
    +
    @Grapes(
    +@Grab(group='com.example', module='Main', version='1.0.0-beta')
    +)
    +

    Gradle/Grails

    +
    +
    implementation 'com.example:Main:1.0.0-beta'
    +

    Scala SBT

    +
    +
    libraryDependencies += "com.example" % "Main" % "1.0.0-beta"
    +

    Leiningen

    +
    +
    [com.example/Main "1.0.0-beta"]
    +
    +
    +
    +
    +
    + + + diff --git a/target/site/images/close.gif b/target/site/images/close.gif new file mode 100644 index 0000000000000000000000000000000000000000..1c26bbc5264fcc943ad7b5a0f1a84daece211f34 GIT binary patch literal 279 zcmZ?wbhEHb6kyFkwP}e}6+mLp=yE)H5&?6cps==O-j2#K*@61O)i|`#U%|*xTD17#Qg5 z>nkWI$ji$M2ng`=^D}^ygDj#2&;c6F0P+h1n~g(5frm~PL&uV$l`S$eFDwzBDbhJD v>}Bvw*Al_tWna1PC9OaGVdk23i}vRhZI{iR^*V|n<^22a#~T_O9T}_vbswrX literal 0 HcmV?d00001 diff --git a/target/site/images/collapsed.gif b/target/site/images/collapsed.gif new file mode 100644 index 0000000000000000000000000000000000000000..6e710840640c1bfd9dd76ce7fef56f1004092508 GIT binary patch literal 53 ycmZ?wbhEHbWM^P!XkdT>#h)yUTnvm1Iv_qshJlI4r7uBZ*YkPFU8d4p4Aua}2?(?R literal 0 HcmV?d00001 diff --git a/target/site/images/expanded.gif b/target/site/images/expanded.gif new file mode 100644 index 0000000000000000000000000000000000000000..0fef3d89e0df1f8bc49a0cd827f2607c7d7fd2f0 GIT binary patch literal 52 xcmZ?wbhEHbWM^P!XkdT>#h)yUTnvm1Iv_qshJlH@g}+fUi&t{amUB!D)&R0C2fzRT literal 0 HcmV?d00001 diff --git a/target/site/images/external.png b/target/site/images/external.png new file mode 100644 index 0000000000000000000000000000000000000000..3f999fc88b360074e41f38c3b4bc06ccb3bb7cf8 GIT binary patch literal 230 zcmeAS@N?(olHy`uVBq!ia0vp^+(699!3-oX?^2ToQY`6?zK#qG>ra@ocD)4hB}-f* zN`mv#O3D+9QW+dm@{>{(JaZG%Q-e|yQz{EjrrH1%@dWsUxR#cd{{R1fCIbVIy!atN z8e~{WkY6y6%iy53@(Yk3;OXKRQgJIOfsI*BO@UFsfhWLBc>*(#PB?Jn2*(o!76E4F z2oaVU3``tH+Kgs0GI5+@Tg}d)z%jd%F@?{8!SRZ5b1yT80-FZIMn)zc2Ca66y`pzY R*nwsJMCn#OVEqF*oew~oaAu*+mN;-=y?VHT3tIe$XQqrDo-uB_a z!$aaK`z6))OKGn34?nwc^SuifkIL#EmDgV_qjg-#8v*0u4q4%1moUw{LZ54UeCgzNF^jX`uv-XK+9g@yFrG9?@ z!9&5&Tgk*j(b!GF&{N4I-Owl3GNQ;Kslp@APSw&&&ux9d>WxL~{EYoKm2KHvv3+ax zZUYB?Ae*8JnchZheXeEaa>@87?_fB*jV>(`erUx0B6j@wa!KnN)QWMO1rn9HC8 zQU}Tt3>@bftT|;oHYhlHH8T8tc{qL2LBC1&wnQeg^-S05<#H=J%;q~&KX!$OXH$lP zifQJ#9>L8|xhAVRHT-xPa*}7JK>(A*!AmL!CQC~j>707p+C5b#ib-SZ5@wfn#-0y8 zor_pb3M^%mkXhlduwjw4dk@RWhYZ<*tSUAV9x3eYyi#^d39lH{872xT#>g14FgCZb z+Lvv}DClhGVU*`8y(Qe}(9I>Lw<6->0~Q`zX3oMH2272dBARI`0wDzxS_G8b_H+a` TZ#n2*^y*Bf^Krq04Gh)*dSnrT literal 0 HcmV?d00001 diff --git a/target/site/images/icon_info_sml.gif b/target/site/images/icon_info_sml.gif new file mode 100644 index 0000000000000000000000000000000000000000..c6cb9ad7ce438a798426703e86a7ffc197d51dbb GIT binary patch literal 606 zcmZ?wbhEHb!Rj)7jHhhdgsOUdoQoueZi?7 z>>gViTe&E#V48n=mrru5S3;v}WQB8hiDz7$TU2Fg8RZkU)J)l4H+4sO@7jjxJ4?G(<~7c1nYFul=C0P+d#d`@bj{yi z-npcE!T#Qb2PP~z)H;3B%r(bntUlH>Y2~CvyV|C%UbyM>vTf&9?!2&e&!siHFV0_c zVB`KP8}?n^dg$7Yqc`@PxOMQ%-NWbZ9Xfk=)1K2OFF!hV;r{6>kIr6ua^~ve%eS9j zy7lbD`I|4_et!J??bq+WzI^-n`RfmdkOIfh!pgqYwSCK`t~@$#!^!1aj_y2mzyI{@?vuB79>2N$==JkApPs$`_~ygc*YCf)diVLp z{pXKfy#M&+`?nvze*gIk#Q*;N0|qHLXbBUFKUo+V7>XElKuSSz!oa?}p{S|3rL`#` zEj=M8CWV#D$GthOu#hRgfH^NPHz`Z6or!6tudIJkhF|)EqL_SUmH;#E=*;vU)ut4d z*}1MJ+3|6yK5|W*0YQlwY}}E_93D;*P3)($(!#iHyj&dYc$?gAB*f@)n?~7Mn)5Ze zB*b!gs&gB@F*e|Da`5(ac688Lp~TGAEh5PBlHo`4aV}w%hy?;49h(#+>`NXTD0Bjy;4ci{C-1K14rU#4Xoa9{m6qopA9n0cn|!>ecYkij zwyX=!4*mH3EoqLqSGiVbyFqxD(bS8XSDu{6U1jZO70Ic@{~t&7=B^ zBD)NOoAkU&Gy^LQJ5PtV?u{&65}4ZUmfYbweP{LTy^YnAGv=AGa7*6wj}%~b0?7r5!@qH7P%p1*$L z@#{ODxoUwG+WsY)zWExj-aqxpQS(e!bx&6L`u)?tfB$~}{{8*?cVO&*V`-G2NeC$Z zWMO1r=w{FXnGVVm3>>=|#5rX=HY{-DP?VFNPL-%m%>B+*~5-k^-+4*MLFr;tQ0}^rlS-^!^Q`Mx1hrB$jwn&hk~Xk=#Nl+_9Nu|Y$D G!5RQ;-6)O# literal 0 HcmV?d00001 diff --git a/target/site/images/icon_warning_sml.gif b/target/site/images/icon_warning_sml.gif new file mode 100644 index 0000000000000000000000000000000000000000..873bbb52cb9768103c27fbb9a9bac16ac615fce5 GIT binary patch literal 576 zcmZ?wbhEHbB!Sy%bj7w z8LP{2I!WYbmF&-Ixi?j6tD|K1XR2M#l>Aw*aXL%wXS3nYW}{zi=4WzsU5r%E6qx+# za{AThd85YVOsT`KDUrWsBtGknIa3>Sy(4;AS@f^Dxt>-=XPXm#FD(1Lr2hBv=9?3X zZS^!XrNw@)>eiN((2|w-y>{aB1+99DGMA?}+UTggT+(Z*rf8+5x~aWVOGcurtl;&U zIa)H3I&#vwvQjJBn`YHj9iKlB7`)(M#!e{yWMO1rC}Yq8NrU2qfqia6SyOXMYa1sM zM_a34eqyRfcQbQJY;^IYGTuzaxglKLqNQEA}OiQec+sQ#rUUjLqg_MpsPmY43 zsgmVV8EHK$eV-B~6*UcAW2+w%1e4o&9#aAczLGF}PmMg|6J0Ey4q A)Bpeg literal 0 HcmV?d00001 diff --git a/target/site/images/logos/build-by-maven-black.png b/target/site/images/logos/build-by-maven-black.png new file mode 100644 index 0000000000000000000000000000000000000000..919fd0f66a7f713920dd7422035db1c9d484351d GIT binary patch literal 2294 zcmVKOG`!VuDc=fnx$+R6#>c^>b&wcOS?|$!`a}U6ptjU_J zlBA}l*3{J0)YMd0R~Hr*dU$xO^ie1jhYlTLS+=C4#MRYRCX#twGUSD6Il$6AA+=UAlkY(ZF;m4037Yc>v&!1mPsNXdliHV74&z>zUEv=}iC@U)i zfc^XTJ3BiAKvYyzczAd~K){|od(ip)`}f`5-HnZnv$L~Hzqq=(y7Kb!>gsAwPfu@e z@3gcu0LabFC4?{xBNPh18Fpy3+Tr2hfq{Yc_V$w}PjVdhGtMTH$zU){PfznaPmK)? z4KH52=;-KZX=#a#jlFZ{PF7YH!!Q{c8Taqs=Xt)UsK{tE{@>vc{2Hgh!NL0adH}e0 z@19Df^78Tm0ES@zz{SO7Zf@=upJ1_AP_bIAgpih&mWqmsojZ4GG#a&9{f)&Au~_Wm z<0F^L4;(mPHk)-io!M*-3JMa7#VIK%EBy%}_$g6IPEM9cBvPp~K0f}{t5+6_rMbEJ z(xpqcZ{G$0j^p<2+vnuu^bN3MdU`rLJ3Br;9ss7MrVbuFxUjHLQBhGX6WriQ5|M*_w z@5bUDdV71dTCG;AO-@dx@4a~OA{y)K>k+2N$jAo|9?w z?b_+nr`2k;!{M;o?Qh<^`R=>#RtFA0KR<`Vfh)Li;|5+X!otGn&U<@%H*VaBDU;Gf zr_<5=()7Iqfmk>yLj`}084`48Zf?d|M~)mpOHfeI{QNv2WMN?;Dk=&9GBY#LVzb%$ z`};Aq6GAK&OK4~)&U*g*IT{xh7M8K~%9SgtQ-;OG#ZeC5ym=F=X|vf(9h#b&K7RZN z05+S=X0xGjU|@g-%ePwl!GC`7t=5VDruDp`t9rXwq=tAb*88KQqo~N`a#V_oixKzA z%F4dJzL1cRy1F{CSUfW`qfjWeZ{Hpm7>H$yNF>V6&c<>vGBOgU_w@7}J9g~o(WA6z z#sgc0B0VlH4i&T6{Pyiz)FUDys6$s*7rnXCi!3z)!0DGJ5eITHyM2Q|E@qtti{QRD z*nbiZg+h^&lY>QINl6I+oH}*N-Q67kYHMqqoSd*@fE67^695Pa36aTU0HD+95)%{g zFw)c0Gcqy&K&4WxG906$qk6p_b=txpgmiazqaGF(M)NU+!{3cPsc^{*a`Ja$nXfZ@ zhsL%N4whw0OG`2M6&4oG&CQ8KBHBPHC@3f>C|I^a>__(qFp!^RU zV`F0uhl6EVxm><`_ijATmoHz|)ztxjL?XdmSuB<(Po5A$mM!w}C3kdS~ef}W>dub-Hhz&fI`vJ#oXvTST@?6qsxN=r)tz|+%n^XARiL+I)0 z!HGL|?4Z?OC@z>ppO+fmk zEDIk1FgrV2R8&O&@;qNwR)+h@$;nZx)dqvXVzG2}b>-#d_4oHa!G&Dp59OYMg zd;9A2I}{29&+|ObzkB!Y^XJcKjE;^*({SomlT)I^E^_90Q{xPG;bvU;38ml zcng&pTZhKxAmAX-{xuvUBO`bZu-omWrKK8;X6fkl>(@`5I6;GyySuwkDCBv*tE;QE zwH1kg)0Ijk1~{Qms8A@Vadob6a=9D}VUx-9>C-1l1S|^dcDq`w#&Z*k#hB*+K%>#n z=0$)zo8T)X1Ujc}V+Omw8!O@%0GKp7%(fp1ER{;7QYogYiHQlT)w*&q5{X2iP;Ak literal 0 HcmV?d00001 diff --git a/target/site/images/logos/build-by-maven-white.png b/target/site/images/logos/build-by-maven-white.png new file mode 100644 index 0000000000000000000000000000000000000000..7d44c9c2e5742bdf8649ad282f83208f1da9b982 GIT binary patch literal 2260 zcmV;_2rKuAP)4hTLUyOQ{PVbVY5&Y3g!&hN~bnR7}ZgkXUt ziC%zU0gf+&kEv>t|d$x|zXw1mS0D%1b{8z7DF%0wW-8(XBFc`A3vVI|O z^!N97baWg(eE86zLn4uA_wL=Zb@+UKU|=8sJb3V6XlSUctSl!dhm4xd=KJ^W|8h2q zR4NS%3yX+|NKQ`f?d=7Cf`Wo)&z=E5TU%REQIXYZefjbwRvsQ6zIyfQojZ3l8V#{v zv)R(q)39Vr2GBPsa+apV2%%fIZY3ln0Kl+1Y8c*(xe3X6sWFH9kH*UDDLl)ZN`}u~;f9D%P!A2LK5P2`MQl z(b3TuDUC++_U+qm01k;n!Z1u+TwGjS+}X2d^Yil+3Pn;B-~q z{Qdm_z{kf&EEb1^gw)j3R904!x}#RBj~+c578Vv16olc}xpQZGd;7k9`>@WHD_2M| z{%VB2fNVCK&1U^_rTW_bx`C@MK&%ZR^ybZ*=;&yb zN);0mV>X+~OA`|lRVtNAr7A8i#zL)DyJycHxm+$5izO0?QmM?$%p@6le0*H3R;yI1 z=;-LCrlu1oPI!8HIypHhmCA~Wig|;>WHON!GbSbmcN`jxhJ=GssnlpRR;zVzaF8J4 z>+3sJhW@0w{LH6-`(Afr<9kMWBXoSUM7Dox&JGJtojOI96z3EG z*uH)HWN?qO7x!`hzQnzLg5JL3Ui^ps%X$n4`+YK2S-yNZo>gC8kJmXUC#D?-i_a7IlwdR(Kkw#T>s)<( zJ!ZVTycREBO!{t;H9|r{F#q)FQ_`LjAsBnPnnKk2PZ;V3*7{M#@%jyBNObh|^_fg2 zd|f0I3eTTEPf=83VhUbHWgRft|{%MRRMp6H>seM7wV6&k5Vn7H0DDSDT_wn(;aaUDU zWi%QoiptK;CgqIWB$bwy78Mm?w@oI~&6_tPBO~$kExCLno}10)mX;RGM?^%-PjqOt zTFi(#=@4C7NJmxEVK7l6G0yhEp_Lq9)1fj}S-2%Mdrv$L~tStVt%xVSheDG9e5EX$6J zj8GIMm&=bIKaK;TqoYG05D0}r0!Kqb1E0?q2n1`_uAR{_f0E{OgnR$~y~Sd|+0n_# z2@6L?MsUQ^H0|QzLJoDKqobtlneyk|8`Sp{cp}PUC5RRQ^8?;2;Iss$eWk%*n3$Nr z(73v~e)3}s219#$yTM=(2n6o#?!LahxUO>?H!v`O%bZ*;$Ideh!!Qg0h{fVXix$lf i91DLtEx@rr0RIK2cl{g~?Z1Nn0000}s literal 0 HcmV?d00001 diff --git a/target/site/images/logos/maven-feather.png b/target/site/images/logos/maven-feather.png new file mode 100644 index 0000000000000000000000000000000000000000..b5ada836e9eb4af4db810f648b013933e72c8fbe GIT binary patch literal 3330 zcmX9>c{JN;_x~o5Ac>t)`_^PEV{L6MNl>(?QcG&7ly=N-Xep}HlEki6%d`xGQff?J zZ3V5?nxMK^TW!%rlc2Oi#TE&YeBaFbd(OGfJqdI` zc>}=J0{}qD0)QP*?7suRWeWiKhXeo)6#$?b`+NA18vvk_kGT^3lRrj~)ZiX~E=7&X z2SKm_0zsnO+$cbVdd$U-?NJjv4pVQ1Nhjly1q-WLl67`_;z%v-QHPc;g_!S~IRE^{ z!-r;4Azogl1_mw!0>pbvoPqVZ9U2s5dwy6sHa1p4L7^@xJ3CvqEtc6=V;Sjo`SKw` zH=oaUc5x93g$)f2RLqLwrQCI9Ez?$q{#(_7txem8O7-r(E=u3NrnVzb>g3;N!E`D4 z$F(MEarBhUUxI^!j~_>3u~Bhx7JsSR*w|dSa6vbc*_R&srRM|ftV?XHdFb}1C$WrQ zvCqw{t=r+KeZT{28=Et|SGiR|Ew_)PCPc7HL$FRx^tIjT!gS^&HZAG+)pJ^j_L!yB z-&JbQI5tJZ0TS}9l}GV-#=yY9@UZdW!+Wo8V)3OP+M~kh8Cox&UgiEXkb|OHrtnt7 z^5^7qoPgd(mzSp^UljFw^Ea1#($jleS~zn<*Qt%~?;g8p7T$+e1_e6_0RivD9i_fn zntBj|S0D{TF>ZC0BjrC=O}^<#pa0LS&uvarfWzp2`pUd__f_%7YV~7dt=r6SgMYpk zjT&tozdBVDfMU+}3PBKu{I@a0eE%y;<26%LfpraXnsz78oRL+ASlucsJ9Ov}^-cnR z?X0S*D(PH#SsA1;IVGjHr-u@pc=<9LQ|*-QU~8*d0k5yGUszbEsHmW5uYUjj;c@h| zc=i>Ql~f4Q{2jFogTeH_k#4q)N#10=x?L3lT5fn+n;f?)a5}#)D(b9?5F`jW*8R2B zY10|kzu50Yt-pEkr?pP=J)v#j+39IETXnv??EKOqdr`^I$PR$!&#+i*wr^07q=V|W zRr`cRLkwol7wvCgY>XVWV#HBVP$e>vs8#}bhe8j(d*@G*O1g5TCFF^jnVIZQvS`z% z5v0FEpQe3XqLbN{Z+4@!!}?n1jYn$VqUAWElr$a=d)NRcr?dxiBP0c$a4eq)C6kW} zg`-#3YZthl;XEcu_;g!xn!}4v15@n5*WxOpB14=8A8Dk>`K z>FLRD7bsziv>lNxci1YB3`T!HV#jF&kvayv7^9-Sg&l|eQ^qB(FU%g~JDx-!K6@(Waovi+Tc$s`@s@Sv* z9p0C*!~5#c{h1>d>@N5DL);Ea=d|PU4}@o zGdG0Ng%R<9V_jn-yfB3nD7kxXb8!sMIXlJ1WeD*5?60hT&XSa)+yVTVl9iP_o8v^w8_0650v?-3$V0uILqsvdAu+2y6|YCewgNhga^h4Y-lNq0Cah}ivo zpoq6EpmWSceZAoF%B5UfVPU3op{AfPhFM{FSFJMU!)c~SDTMch@trf6$~-E;5xn-d z<8`e~UPj0w%vDYVje(iQii)`c=wzHbR6^djAF^dnW5A}!CD-JMWyVHEkW;BwukLPq z9nsR%B=!TuB0vQ|DPO#J@zkle(n^?>&z)~)XSMt|Ks2+uT9af6QEqK-hanLX5&&xP z-l-<%m`WTuBR<~hh#iYkQxoQNXtTFvX)i0JF_1Iu5Wn+7^XJlfPFX+T%IM9_7+4B=%5Y=a!X6S`QV)~knSitusE`|vEgD?+D*SdgtN-v z@2!tnPsQ$W9OoldXg5!7EGfyuKEmbk%8!pz518D&%P>a8*ji>n+N5Y15QI!N3aw76 zk?~TlC_r^z21V(@jrIB2O=fW{*e;OxLwTOl%b7{65NYoUzv46uU?y1WK`h1$gXk#s zGM!NC1T6)2&vea(*Gjoe-Y0OseT68UKVi7GtWs>+{mTm3?9wmCl9JqVL7fcIg7PHy zS|uV8fd^!W2I;)j*_@ml#-BrjgIWH)bTI&Jf1fXAax!YjYcdmoW44Np%MhjRZR?D*fO!{1UqRj~p#EAohT=T-17$$k6AmQb( zr9h0V!aUsY=NL_BPmf|~=n=+2*+gqRK=3w1+z;yxltfUx%}G^AqM7qBoD>Zu#))>h z(O-H}7=Go_Xv&X~RNksk#{u}JDqbNyJIauD&lJ!>cpV`%&T(-`&1Vx}= z8{BIG$r-+Li5}_#{j}s%FlGk$jM1|WKp=Pv|*T=m!~I+rUjJ3F@7W!gumQD8RFwVZryr0 zG6IWssk0)%eJuVTRDtKPo&xDaOWF|RzCnozye=JYW-)oDFHKrbK}AL7sWkcH57B~D zWIZ`=QNK#g)SEJB!`69JGO3P=r08pDX))Bb6t@_;R!2TlYhv>Ek*cIBeDucB zNbDTV5C(L01Ze7}3Kc7OC~(zLdAV~G`9N+1xB3ie(wD=k6U z@g3gU065J9XPq{lyp>keB&(ixxdnV8$%i$asL6b0O)JUdYtCpuubGB*DbEFHXlQtp zXgMTG%@{+j0dI{Adnj6-$)BcQylA>}r~l(e_1pE-*`Eac5PAGF#EWMIO6;2ECZAeo ziPF85kd7Ft6f{I>ZQIUbf5YND4#d%gJpKl~IaM@Xl!bUvZj*0lQRvUOOhugnVG zMF7OiLdS5a+otCLNQI8V^8vu3ka8NP_S>32`v3S)2n{Pe(fRVLdLST=H+AiBqCTY3 zZWI=>Zsgp=`Z%jG=8)QMYZO=@1A#!)z2kiwpnq3DhkpUGZV&>CeaB0vA>Y6+Mrd+| zrA52d@P7Qe=6m=0Lz-`5yrGM(x*9Y0sP7_5T2*v`@~JgS7L3#>yY-7x_MJ+9`9JqyEa*$Q0 ziiL%hken<6A7+&3D;!0f@qP3TvIRVoufv)c8?&aw&B~1Y(02aUpDjK7B)cSkx8QDV zQMj_M+x+$UXOfa)nmweB@KP^Xm2R7$9(p;LCnufvW}*eG4R>Eak)Ei}%-KE8gsec^ zj=HuX z(qyBjd`DTC3ZeF2!np?{CKA-DtE=Op^zuqOJMFU}UTntQB1KKp81%{!bT~6heKA2v zt?`kF-Zi+k^YcNCz>V!+^RbV}r|Gp2j0+=crL`N5t}4tX=Ugo&7+C6ua?F4oX!wQ+)83@^vkY zDLFc>n(A(&_r09T&@t7l6XQ+b#6#=gA#14-D;h1Uq<(+=C8$D8`D^qmZ z9NOcdL`OIEho{GDl585|eQ0-*j0e6Rr=PNtyozBAqJr literal 0 HcmV?d00001 diff --git a/target/site/images/newwindow.png b/target/site/images/newwindow.png new file mode 100644 index 0000000000000000000000000000000000000000..6287f72bd08a870908e7361d98c35ee0d6dcbc82 GIT binary patch literal 220 zcmeAS@N?(olHy`uVBq!ia0vp^+(699!3-oX?^2ToQY`6?zK#qG>ra@ocD)4hB}-f* zN`mv#O3D+9QW+dm@{>{(JaZG%Q-e|yQz{EjrrH1%@dWsUxR#cd&SYTt4+aeuCvSob zD+%%o1`04ZXs!GLj7%Iec?BF2%&y2ZFfeUwWbk2P5nvW+xWT~4#-PT{uyM;F);OSv44$rjF6*2U FngH~|K)3(^ literal 0 HcmV?d00001 diff --git a/target/site/index.html b/target/site/index.html new file mode 100644 index 0000000..d9b6ac1 --- /dev/null +++ b/target/site/index.html @@ -0,0 +1,75 @@ + + + + + + + + Vision Skills Progression Tracker – About + + + + + + + + +
    + +
    +
    +
    +
    +

    About Vision Skills Progression Tracker

    +

    Vision Skills Progression Tracker

    +
    +
    +
    +
    +
    + + + diff --git a/target/site/licenses.html b/target/site/licenses.html new file mode 100644 index 0000000..762003e --- /dev/null +++ b/target/site/licenses.html @@ -0,0 +1,282 @@ + + + + + + + + Vision Skills Progression Tracker – Project Licenses + + + + + + + + +
    + +
    +
    +
    +
    +

    Overview

    +

    Typically the licenses listed for the project are that of the project itself, and not of dependencies.

    +

    Project Licenses

    +

    Apache-2.0

    +

    A business-friendly OSS license

    +
    +
    +                                 Apache License
    +                           Version 2.0, January 2004
    +                        http://www.apache.org/licenses/
    +
    +   TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
    +
    +   1. Definitions.
    +
    +      "License" shall mean the terms and conditions for use, reproduction,
    +      and distribution as defined by Sections 1 through 9 of this document.
    +
    +      "Licensor" shall mean the copyright owner or entity authorized by
    +      the copyright owner that is granting the License.
    +
    +      "Legal Entity" shall mean the union of the acting entity and all
    +      other entities that control, are controlled by, or are under common
    +      control with that entity. For the purposes of this definition,
    +      "control" means (i) the power, direct or indirect, to cause the
    +      direction or management of such entity, whether by contract or
    +      otherwise, or (ii) ownership of fifty percent (50%) or more of the
    +      outstanding shares, or (iii) beneficial ownership of such entity.
    +
    +      "You" (or "Your") shall mean an individual or Legal Entity
    +      exercising permissions granted by this License.
    +
    +      "Source" form shall mean the preferred form for making modifications,
    +      including but not limited to software source code, documentation
    +      source, and configuration files.
    +
    +      "Object" form shall mean any form resulting from mechanical
    +      transformation or translation of a Source form, including but
    +      not limited to compiled object code, generated documentation,
    +      and conversions to other media types.
    +
    +      "Work" shall mean the work of authorship, whether in Source or
    +      Object form, made available under the License, as indicated by a
    +      copyright notice that is included in or attached to the work
    +      (an example is provided in the Appendix below).
    +
    +      "Derivative Works" shall mean any work, whether in Source or Object
    +      form, that is based on (or derived from) the Work and for which the
    +      editorial revisions, annotations, elaborations, or other modifications
    +      represent, as a whole, an original work of authorship. For the purposes
    +      of this License, Derivative Works shall not include works that remain
    +      separable from, or merely link (or bind by name) to the interfaces of,
    +      the Work and Derivative Works thereof.
    +
    +      "Contribution" shall mean any work of authorship, including
    +      the original version of the Work and any modifications or additions
    +      to that Work or Derivative Works thereof, that is intentionally
    +      submitted to Licensor for inclusion in the Work by the copyright owner
    +      or by an individual or Legal Entity authorized to submit on behalf of
    +      the copyright owner. For the purposes of this definition, "submitted"
    +      means any form of electronic, verbal, or written communication sent
    +      to the Licensor or its representatives, including but not limited to
    +      communication on electronic mailing lists, source code control systems,
    +      and issue tracking systems that are managed by, or on behalf of, the
    +      Licensor for the purpose of discussing and improving the Work, but
    +      excluding communication that is conspicuously marked or otherwise
    +      designated in writing by the copyright owner as "Not a Contribution."
    +
    +      "Contributor" shall mean Licensor and any individual or Legal Entity
    +      on behalf of whom a Contribution has been received by Licensor and
    +      subsequently incorporated within the Work.
    +
    +   2. Grant of Copyright License. Subject to the terms and conditions of
    +      this License, each Contributor hereby grants to You a perpetual,
    +      worldwide, non-exclusive, no-charge, royalty-free, irrevocable
    +      copyright license to reproduce, prepare Derivative Works of,
    +      publicly display, publicly perform, sublicense, and distribute the
    +      Work and such Derivative Works in Source or Object form.
    +
    +   3. Grant of Patent License. Subject to the terms and conditions of
    +      this License, each Contributor hereby grants to You a perpetual,
    +      worldwide, non-exclusive, no-charge, royalty-free, irrevocable
    +      (except as stated in this section) patent license to make, have made,
    +      use, offer to sell, sell, import, and otherwise transfer the Work,
    +      where such license applies only to those patent claims licensable
    +      by such Contributor that are necessarily infringed by their
    +      Contribution(s) alone or by combination of their Contribution(s)
    +      with the Work to which such Contribution(s) was submitted. If You
    +      institute patent litigation against any entity (including a
    +      cross-claim or counterclaim in a lawsuit) alleging that the Work
    +      or a Contribution incorporated within the Work constitutes direct
    +      or contributory patent infringement, then any patent licenses
    +      granted to You under this License for that Work shall terminate
    +      as of the date such litigation is filed.
    +
    +   4. Redistribution. You may reproduce and distribute copies of the
    +      Work or Derivative Works thereof in any medium, with or without
    +      modifications, and in Source or Object form, provided that You
    +      meet the following conditions:
    +
    +      (a) You must give any other recipients of the Work or
    +          Derivative Works a copy of this License; and
    +
    +      (b) You must cause any modified files to carry prominent notices
    +          stating that You changed the files; and
    +
    +      (c) You must retain, in the Source form of any Derivative Works
    +          that You distribute, all copyright, patent, trademark, and
    +          attribution notices from the Source form of the Work,
    +          excluding those notices that do not pertain to any part of
    +          the Derivative Works; and
    +
    +      (d) If the Work includes a "NOTICE" text file as part of its
    +          distribution, then any Derivative Works that You distribute must
    +          include a readable copy of the attribution notices contained
    +          within such NOTICE file, excluding those notices that do not
    +          pertain to any part of the Derivative Works, in at least one
    +          of the following places: within a NOTICE text file distributed
    +          as part of the Derivative Works; within the Source form or
    +          documentation, if provided along with the Derivative Works; or,
    +          within a display generated by the Derivative Works, if and
    +          wherever such third-party notices normally appear. The contents
    +          of the NOTICE file are for informational purposes only and
    +          do not modify the License. You may add Your own attribution
    +          notices within Derivative Works that You distribute, alongside
    +          or as an addendum to the NOTICE text from the Work, provided
    +          that such additional attribution notices cannot be construed
    +          as modifying the License.
    +
    +      You may add Your own copyright statement to Your modifications and
    +      may provide additional or different license terms and conditions
    +      for use, reproduction, or distribution of Your modifications, or
    +      for any such Derivative Works as a whole, provided Your use,
    +      reproduction, and distribution of the Work otherwise complies with
    +      the conditions stated in this License.
    +
    +   5. Submission of Contributions. Unless You explicitly state otherwise,
    +      any Contribution intentionally submitted for inclusion in the Work
    +      by You to the Licensor shall be under the terms and conditions of
    +      this License, without any additional terms or conditions.
    +      Notwithstanding the above, nothing herein shall supersede or modify
    +      the terms of any separate license agreement you may have executed
    +      with Licensor regarding such Contributions.
    +
    +   6. Trademarks. This License does not grant permission to use the trade
    +      names, trademarks, service marks, or product names of the Licensor,
    +      except as required for reasonable and customary use in describing the
    +      origin of the Work and reproducing the content of the NOTICE file.
    +
    +   7. Disclaimer of Warranty. Unless required by applicable law or
    +      agreed to in writing, Licensor provides the Work (and each
    +      Contributor provides its Contributions) on an "AS IS" BASIS,
    +      WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
    +      implied, including, without limitation, any warranties or conditions
    +      of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
    +      PARTICULAR PURPOSE. You are solely responsible for determining the
    +      appropriateness of using or redistributing the Work and assume any
    +      risks associated with Your exercise of permissions under this License.
    +
    +   8. Limitation of Liability. In no event and under no legal theory,
    +      whether in tort (including negligence), contract, or otherwise,
    +      unless required by applicable law (such as deliberate and grossly
    +      negligent acts) or agreed to in writing, shall any Contributor be
    +      liable to You for damages, including any direct, indirect, special,
    +      incidental, or consequential damages of any character arising as a
    +      result of this License or out of the use or inability to use the
    +      Work (including but not limited to damages for loss of goodwill,
    +      work stoppage, computer failure or malfunction, or any and all
    +      other commercial damages or losses), even if such Contributor
    +      has been advised of the possibility of such damages.
    +
    +   9. Accepting Warranty or Additional Liability. While redistributing
    +      the Work or Derivative Works thereof, You may choose to offer,
    +      and charge a fee for, acceptance of support, warranty, indemnity,
    +      or other liability obligations and/or rights consistent with this
    +      License. However, in accepting such obligations, You may act only
    +      on Your own behalf and on Your sole responsibility, not on behalf
    +      of any other Contributor, and only if You agree to indemnify,
    +      defend, and hold each Contributor harmless for any liability
    +      incurred by, or claims asserted against, such Contributor by reason
    +      of your accepting any such warranty or additional liability.
    +
    +   END OF TERMS AND CONDITIONS
    +
    +   APPENDIX: How to apply the Apache License to your work.
    +
    +      To apply the Apache License to your work, attach the following
    +      boilerplate notice, with the fields enclosed by brackets "[]"
    +      replaced with your own identifying information. (Don't include
    +      the brackets!)  The text should be enclosed in the appropriate
    +      comment syntax for the file format. We also recommend that a
    +      file or class name and description of purpose be included on the
    +      same "printed page" as the copyright notice for easier
    +      identification within third-party archives.
    +
    +   Copyright [yyyy] [name of copyright owner]
    +
    +   Licensed under the Apache License, Version 2.0 (the "License");
    +   you may not use this file except in compliance with the License.
    +   You may obtain a copy of the License at
    +
    +       http://www.apache.org/licenses/LICENSE-2.0
    +
    +   Unless required by applicable law or agreed to in writing, software
    +   distributed under the License is distributed on an "AS IS" BASIS,
    +   WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
    +   See the License for the specific language governing permissions and
    +   limitations under the License.
    +
    +
    +
    +
    +
    +
    + + + diff --git a/target/site/plugins.html b/target/site/plugins.html new file mode 100644 index 0000000..09ecf7a --- /dev/null +++ b/target/site/plugins.html @@ -0,0 +1,149 @@ + + + + + + + + Vision Skills Progression Tracker – Project Plugins + + + + + + + + +
    + +
    +
    +
    +
    +

    Project Build Plugins

    + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
    GroupIdArtifactIdVersion
    com.github.spotbugsspotbugs-maven-plugin4.7.3.2
    org.apache.maven.pluginsmaven-checkstyle-plugin3.4.0
    org.apache.maven.pluginsmaven-clean-plugin3.2.0
    org.apache.maven.pluginsmaven-compiler-plugin3.13.0
    org.apache.maven.pluginsmaven-deploy-plugin3.1.2
    org.apache.maven.pluginsmaven-install-plugin3.1.2
    org.apache.maven.pluginsmaven-jar-plugin3.4.1
    org.apache.maven.pluginsmaven-javadoc-plugin3.8.0
    org.apache.maven.pluginsmaven-resources-plugin3.3.1
    org.apache.maven.pluginsmaven-shade-plugin3.6.0
    org.apache.maven.pluginsmaven-site-plugin3.12.1
    org.apache.maven.pluginsmaven-surefire-plugin3.0.0-M7
    +

    Project Report Plugins

    + + + + + + + + + + + + + + + + + + + + +
    GroupIdArtifactIdVersion
    org.apache.maven.pluginsmaven-checkstyle-plugin3.4.0
    org.apache.maven.pluginsmaven-javadoc-plugin3.8.0
    org.apache.maven.pluginsmaven-project-info-reports-plugin3.4.5
    org.apache.maven.pluginsmaven-surefire-report-plugin3.5.0
    +
    +
    +
    +
    +
    + + + diff --git a/target/site/project-info.html b/target/site/project-info.html new file mode 100644 index 0000000..749201b --- /dev/null +++ b/target/site/project-info.html @@ -0,0 +1,98 @@ + + + + + + + + Vision Skills Progression Tracker – Project Information + + + + + + + + +
    + +
    +
    +
    +
    +

    Project Information

    +

    This document provides an overview of the various documents and links that are part of this project's general information. All of this content is automatically generated by Maven on behalf of the project.

    +

    Overview

    + + + + + + + + + + + + + + + + + + + + + +
    DocumentDescription
    AboutVision Skills Progression Tracker
    SummaryThis document lists other related information of this project
    LicensesThis document lists the project license(s).
    DependenciesThis document lists the project's dependencies and provides information on each dependency.
    Dependency InformationThis document describes how to include this project as a dependency using various dependency management tools.
    PluginsThis document lists the build plugins and the report plugins used by this project.
    +
    +
    +
    +
    +
    + + + diff --git a/target/site/project-reports.html b/target/site/project-reports.html new file mode 100644 index 0000000..780f51a --- /dev/null +++ b/target/site/project-reports.html @@ -0,0 +1,90 @@ + + + + + + + + Vision Skills Progression Tracker – Generated Reports + + + + + + + + +
    + +
    +
    +
    +
    +

    Generated Reports

    +

    This document provides an overview of the various reports that are automatically generated by Maven . Each report is briefly described below.

    +

    Overview

    + + + + + + + + + + + + + + + +
    DocumentDescription
    JavadocJavadoc API documentation.
    Test JavadocTest Javadoc API documentation.
    SurefireReport on the test results of the project.
    CheckstyleReport on coding style conventions.
    +
    +
    +
    +
    +
    + + + diff --git a/target/site/summary.html b/target/site/summary.html new file mode 100644 index 0000000..fa1302c --- /dev/null +++ b/target/site/summary.html @@ -0,0 +1,119 @@ + + + + + + + + Vision Skills Progression Tracker – Project Summary + + + + + + + + +
    + +
    +
    +
    +
    +

    Project Summary

    +

    Project Information

    + + + + + + + + + + + + +
    FieldValue
    NameVision Skills Progression Tracker
    DescriptionVision Skills Progression Tracker
    Homepage-
    +

    Project Organization

    + + + + + + + + + +
    FieldValue
    NameMichael Ryan Hunsaker, M.Ed., Ph.D.
    URLhttps://github.com/mrhunsaker/
    +

    Build Information

    + + + + + + + + + + + + + + + + + + +
    FieldValue
    GroupIdcom.example
    ArtifactIdMain
    Version1.0.0-beta
    Typejar
    Java Version-
    +
    +
    +
    +
    +
    + + + diff --git a/target/site/surefire.html b/target/site/surefire.html new file mode 100644 index 0000000..a033243 --- /dev/null +++ b/target/site/surefire.html @@ -0,0 +1,323 @@ + + + + + + + + Vision Skills Progression Tracker – Surefire Report + + + + + + + + +
    + +
    +
    +
    + + +
    +

    Surefire Report

    +

    Summary

    +

    [Summary] [Package List] [Test Cases]


    + + + + + + + + + + + + + + +
    TestsErrorsFailuresSkippedSuccess RateTime
    11000100%2.532 s

    +

    Note: failures are anticipated and checked for with assertions while errors are unanticipated.


    +

    Package List

    +

    [Summary] [Package List] [Test Cases]


    + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
    PackageTestsErrorsFailuresSkippedSuccess RateTime
    com.studentgui.apphelpers3000100%0.404 s
    com.studentgui.apppages1000100%0.106 s
    com.studentgui.test7000100%2.022 s

    +

    Note: package statistics are not computed recursively, they only sum up all of its testsuites numbers.

    +

    com.studentgui.apphelpers

    + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
    -ClassTestsErrorsFailuresSkippedSuccess RateTime
    DatabaseContactLogTest1000100%0.286 s
    SessionJsonWriterTest1000100%0.115 s
    SqlGenerateTest1000100%0.003 s
    +

    com.studentgui.apppages

    + + + + + + + + + + + + + + + + + + +
    -ClassTestsErrorsFailuresSkippedSuccess RateTime
    JLineGraphDeterministicJitterTest1000100%0.106 s
    +

    com.studentgui.test

    + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
    -ClassTestsErrorsFailuresSkippedSuccess RateTime
    BrailleDatabaseTest1000100%0.134 s
    BrailleSmokeTest1000100%0.608 s
    DatabaseEdgeCasesTest3000100%0.124 s
    DatabaseTest1000100%0.088 s
    ExportBrailleReportsTest1000100%1.068 s

    +

    Test Cases

    +

    [Summary] [Package List] [Test Cases]

    +

    DatabaseContactLogTest

    + + + + +
    testSaveAndFetchContactLog0.273 s
    +

    SessionJsonWriterTest

    + + + + +
    writeSessionJson_includesSessionIdAndPayload0.113 s
    +

    SqlGenerateTest

    + + + + +
    testInitializeCreatesContactLogTable0.003 s
    +

    JLineGraphDeterministicJitterTest

    + + + + +
    deterministicJitterProducesSameSequence0.105 s
    +

    BrailleDatabaseTest

    + + + + +
    smokeDatabaseFlow0.134 s
    +

    BrailleSmokeTest

    + + + + +
    smokeTestDatabaseAndGraph0.607 s
    +

    DatabaseEdgeCasesTest

    + + + + + + + + + + + + +
    ensureAssessmentPartsIsIdempotentAndIgnoresUnknownPartsOnInsert0.085 s
    saveSessionNotesPersistsNotes0.024 s
    duplicateStudentNamesReturnSameId0.001 s
    +

    DatabaseTest

    + + + + +
    testStudentCreateAndFetch0.077 s
    +

    ExportBrailleReportsTest

    + + + + +
    generateBrailleExport1.067 s

    +
    +
    +
    +
    +
    + + + diff --git a/target/site/testapidocs/allclasses-index.html b/target/site/testapidocs/allclasses-index.html new file mode 100644 index 0000000..296e9ed --- /dev/null +++ b/target/site/testapidocs/allclasses-index.html @@ -0,0 +1,103 @@ + + + + +All Classes and Interfaces (Vision Skills Progression Tracker 1.0.0-beta Test API) + + + + + + + + + + + + + + +
    + +
    +
    +
    +

    All Classes and Interfaces

    +
    +
    +
    Classes
    +
    +
    Class
    +
    Description
    + +
    +
    Small integration-style unit test that uses the normalized Database helper methods + to create a student, a progress type, ensure parts, insert one session and fetch the + latest results.
    +
    + +
    +
    JUnit replacement for the legacy Braille smoke main.
    +
    + +
     
    + +
     
    + +
    +
    Basic integration tests for the Database helper using the on-disk sqlite + created in the project's application data folder.
    +
    + +
    +
    Test that generates example Braille exports (per-phase PNGs + MD/HTML) + for the student "Test Student".
    +
    + +
    +
    Small unit test to validate deterministic jitter reproducibility.
    +
    + +
    +
    Unit test for SessionJsonWriter to verify envelope and filename format.
    +
    + +
     
    +
    +
    +
    + +
    +
    + + diff --git a/target/site/testapidocs/allpackages-index.html b/target/site/testapidocs/allpackages-index.html new file mode 100644 index 0000000..eec8133 --- /dev/null +++ b/target/site/testapidocs/allpackages-index.html @@ -0,0 +1,73 @@ + + + + +All Packages (Vision Skills Progression Tracker 1.0.0-beta Test API) + + + + + + + + + + + + + + +
    + +
    +
    +
    +

    All Packages

    +
    +
    Package Summary
    + +
    + +
    +
    + + diff --git a/target/site/testapidocs/com/studentgui/apphelpers/DatabaseContactLogTest.html b/target/site/testapidocs/com/studentgui/apphelpers/DatabaseContactLogTest.html new file mode 100644 index 0000000..53ed4f1 --- /dev/null +++ b/target/site/testapidocs/com/studentgui/apphelpers/DatabaseContactLogTest.html @@ -0,0 +1,183 @@ + + + + +DatabaseContactLogTest (Vision Skills Progression Tracker 1.0.0-beta Test API) + + + + + + + + + + + + + + +
    + +
    +
    + +
    + +

    Class DatabaseContactLogTest

    +
    +
    java.lang.Object +
    com.studentgui.apphelpers.DatabaseContactLogTest
    +
    +
    +
    +
    public class DatabaseContactLogTest +extends Object
    +
    +
    + +
    +
    + +
    + +
    + +
    +
    + + diff --git a/target/site/testapidocs/com/studentgui/apphelpers/SessionJsonWriterTest.html b/target/site/testapidocs/com/studentgui/apphelpers/SessionJsonWriterTest.html new file mode 100644 index 0000000..4bd61e8 --- /dev/null +++ b/target/site/testapidocs/com/studentgui/apphelpers/SessionJsonWriterTest.html @@ -0,0 +1,184 @@ + + + + +SessionJsonWriterTest (Vision Skills Progression Tracker 1.0.0-beta Test API) + + + + + + + + + + + + + + +
    + +
    +
    + +
    + +

    Class SessionJsonWriterTest

    +
    +
    java.lang.Object +
    com.studentgui.apphelpers.SessionJsonWriterTest
    +
    +
    +
    +
    public class SessionJsonWriterTest +extends Object
    +
    Unit test for SessionJsonWriter to verify envelope and filename format.
    +
    +
    + +
    +
    + +
    + +
    + +
    +
    + + diff --git a/target/site/testapidocs/com/studentgui/apphelpers/SqlGenerateTest.html b/target/site/testapidocs/com/studentgui/apphelpers/SqlGenerateTest.html new file mode 100644 index 0000000..fc624e3 --- /dev/null +++ b/target/site/testapidocs/com/studentgui/apphelpers/SqlGenerateTest.html @@ -0,0 +1,183 @@ + + + + +SqlGenerateTest (Vision Skills Progression Tracker 1.0.0-beta Test API) + + + + + + + + + + + + + + +
    + +
    +
    + +
    + +

    Class SqlGenerateTest

    +
    +
    java.lang.Object +
    com.studentgui.apphelpers.SqlGenerateTest
    +
    +
    +
    +
    public class SqlGenerateTest +extends Object
    +
    +
    + +
    +
    + +
    + +
    + +
    +
    + + diff --git a/target/site/testapidocs/com/studentgui/apphelpers/class-use/DatabaseContactLogTest.html b/target/site/testapidocs/com/studentgui/apphelpers/class-use/DatabaseContactLogTest.html new file mode 100644 index 0000000..b81b123 --- /dev/null +++ b/target/site/testapidocs/com/studentgui/apphelpers/class-use/DatabaseContactLogTest.html @@ -0,0 +1,62 @@ + + + + +Uses of Class com.studentgui.apphelpers.DatabaseContactLogTest (Vision Skills Progression Tracker 1.0.0-beta Test API) + + + + + + + + + + + + + + +
    + +
    +
    +
    +

    Uses of Class
    com.studentgui.apphelpers.DatabaseContactLogTest

    +
    +No usage of com.studentgui.apphelpers.DatabaseContactLogTest
    + +
    +
    + + diff --git a/target/site/testapidocs/com/studentgui/apphelpers/class-use/SessionJsonWriterTest.html b/target/site/testapidocs/com/studentgui/apphelpers/class-use/SessionJsonWriterTest.html new file mode 100644 index 0000000..71a9462 --- /dev/null +++ b/target/site/testapidocs/com/studentgui/apphelpers/class-use/SessionJsonWriterTest.html @@ -0,0 +1,62 @@ + + + + +Uses of Class com.studentgui.apphelpers.SessionJsonWriterTest (Vision Skills Progression Tracker 1.0.0-beta Test API) + + + + + + + + + + + + + + +
    + +
    +
    +
    +

    Uses of Class
    com.studentgui.apphelpers.SessionJsonWriterTest

    +
    +No usage of com.studentgui.apphelpers.SessionJsonWriterTest
    + +
    +
    + + diff --git a/target/site/testapidocs/com/studentgui/apphelpers/class-use/SqlGenerateTest.html b/target/site/testapidocs/com/studentgui/apphelpers/class-use/SqlGenerateTest.html new file mode 100644 index 0000000..84148f4 --- /dev/null +++ b/target/site/testapidocs/com/studentgui/apphelpers/class-use/SqlGenerateTest.html @@ -0,0 +1,62 @@ + + + + +Uses of Class com.studentgui.apphelpers.SqlGenerateTest (Vision Skills Progression Tracker 1.0.0-beta Test API) + + + + + + + + + + + + + + +
    + +
    +
    +
    +

    Uses of Class
    com.studentgui.apphelpers.SqlGenerateTest

    +
    +No usage of com.studentgui.apphelpers.SqlGenerateTest
    + +
    +
    + + diff --git a/target/site/testapidocs/com/studentgui/apphelpers/package-summary.html b/target/site/testapidocs/com/studentgui/apphelpers/package-summary.html new file mode 100644 index 0000000..807a543 --- /dev/null +++ b/target/site/testapidocs/com/studentgui/apphelpers/package-summary.html @@ -0,0 +1,102 @@ + + + + +com.studentgui.apphelpers (Vision Skills Progression Tracker 1.0.0-beta Test API) + + + + + + + + + + + + + + +
    + +
    +
    +
    +

    Package com.studentgui.apphelpers

    +
    +
    +
    package com.studentgui.apphelpers
    +
    + +
    +
    + +
    +
    + + diff --git a/target/site/testapidocs/com/studentgui/apphelpers/package-tree.html b/target/site/testapidocs/com/studentgui/apphelpers/package-tree.html new file mode 100644 index 0000000..96dae99 --- /dev/null +++ b/target/site/testapidocs/com/studentgui/apphelpers/package-tree.html @@ -0,0 +1,78 @@ + + + + +com.studentgui.apphelpers Class Hierarchy (Vision Skills Progression Tracker 1.0.0-beta Test API) + + + + + + + + + + + + + + +
    + +
    +
    +
    +

    Hierarchy For Package com.studentgui.apphelpers

    +
    +Package Hierarchies: + +
    +

    Class Hierarchy

    + +
    +
    + +
    +
    + + diff --git a/target/site/testapidocs/com/studentgui/apphelpers/package-use.html b/target/site/testapidocs/com/studentgui/apphelpers/package-use.html new file mode 100644 index 0000000..8bad8ca --- /dev/null +++ b/target/site/testapidocs/com/studentgui/apphelpers/package-use.html @@ -0,0 +1,62 @@ + + + + +Uses of Package com.studentgui.apphelpers (Vision Skills Progression Tracker 1.0.0-beta Test API) + + + + + + + + + + + + + + +
    + +
    +
    +
    +

    Uses of Package
    com.studentgui.apphelpers

    +
    +No usage of com.studentgui.apphelpers
    + +
    +
    + + diff --git a/target/site/testapidocs/com/studentgui/apppages/JLineGraphDeterministicJitterTest.html b/target/site/testapidocs/com/studentgui/apppages/JLineGraphDeterministicJitterTest.html new file mode 100644 index 0000000..10f9010 --- /dev/null +++ b/target/site/testapidocs/com/studentgui/apppages/JLineGraphDeterministicJitterTest.html @@ -0,0 +1,187 @@ + + + + +JLineGraphDeterministicJitterTest (Vision Skills Progression Tracker 1.0.0-beta Test API) + + + + + + + + + + + + + + +
    + +
    +
    + +
    + +

    Class JLineGraphDeterministicJitterTest

    +
    +
    java.lang.Object +
    com.studentgui.apppages.JLineGraphDeterministicJitterTest
    +
    +
    +
    + +
    Small unit test to validate deterministic jitter reproducibility. + The test uses reflection to invoke the private addJitter(double) helper + on two separate JLineGraph instances configured with the same seed and + deterministic mode. The produced sequences must match exactly.
    +
    +
    + +
    +
    + +
    + +
    + +
    +
    + + diff --git a/target/site/testapidocs/com/studentgui/apppages/class-use/JLineGraphDeterministicJitterTest.html b/target/site/testapidocs/com/studentgui/apppages/class-use/JLineGraphDeterministicJitterTest.html new file mode 100644 index 0000000..701e19b --- /dev/null +++ b/target/site/testapidocs/com/studentgui/apppages/class-use/JLineGraphDeterministicJitterTest.html @@ -0,0 +1,62 @@ + + + + +Uses of Class com.studentgui.apppages.JLineGraphDeterministicJitterTest (Vision Skills Progression Tracker 1.0.0-beta Test API) + + + + + + + + + + + + + + +
    + +
    +
    +
    +

    Uses of Class
    com.studentgui.apppages.JLineGraphDeterministicJitterTest

    +
    +No usage of com.studentgui.apppages.JLineGraphDeterministicJitterTest
    + +
    +
    + + diff --git a/target/site/testapidocs/com/studentgui/apppages/package-summary.html b/target/site/testapidocs/com/studentgui/apppages/package-summary.html new file mode 100644 index 0000000..934f7db --- /dev/null +++ b/target/site/testapidocs/com/studentgui/apppages/package-summary.html @@ -0,0 +1,98 @@ + + + + +com.studentgui.apppages (Vision Skills Progression Tracker 1.0.0-beta Test API) + + + + + + + + + + + + + + +
    + +
    +
    +
    +

    Package com.studentgui.apppages

    +
    +
    +
    package com.studentgui.apppages
    +
    + +
    +
    + +
    +
    + + diff --git a/target/site/testapidocs/com/studentgui/apppages/package-tree.html b/target/site/testapidocs/com/studentgui/apppages/package-tree.html new file mode 100644 index 0000000..0bacd01 --- /dev/null +++ b/target/site/testapidocs/com/studentgui/apppages/package-tree.html @@ -0,0 +1,76 @@ + + + + +com.studentgui.apppages Class Hierarchy (Vision Skills Progression Tracker 1.0.0-beta Test API) + + + + + + + + + + + + + + +
    + +
    +
    +
    +

    Hierarchy For Package com.studentgui.apppages

    +
    +Package Hierarchies: + +
    +

    Class Hierarchy

    + +
    +
    + +
    +
    + + diff --git a/target/site/testapidocs/com/studentgui/apppages/package-use.html b/target/site/testapidocs/com/studentgui/apppages/package-use.html new file mode 100644 index 0000000..964c009 --- /dev/null +++ b/target/site/testapidocs/com/studentgui/apppages/package-use.html @@ -0,0 +1,62 @@ + + + + +Uses of Package com.studentgui.apppages (Vision Skills Progression Tracker 1.0.0-beta Test API) + + + + + + + + + + + + + + +
    + +
    +
    +
    +

    Uses of Package
    com.studentgui.apppages

    +
    +No usage of com.studentgui.apppages
    + +
    +
    + + diff --git a/target/site/testapidocs/com/studentgui/test/BrailleDatabaseTest.html b/target/site/testapidocs/com/studentgui/test/BrailleDatabaseTest.html new file mode 100644 index 0000000..7ff3847 --- /dev/null +++ b/target/site/testapidocs/com/studentgui/test/BrailleDatabaseTest.html @@ -0,0 +1,186 @@ + + + + +BrailleDatabaseTest (Vision Skills Progression Tracker 1.0.0-beta Test API) + + + + + + + + + + + + + + +
    + +
    +
    + +
    + +

    Class BrailleDatabaseTest

    +
    +
    java.lang.Object +
    com.studentgui.test.BrailleDatabaseTest
    +
    +
    +
    +
    public class BrailleDatabaseTest +extends Object
    +
    Small integration-style unit test that uses the normalized Database helper methods + to create a student, a progress type, ensure parts, insert one session and fetch the + latest results. This runs headless and doesn't start any UI components.
    +
    +
    + +
    +
    + +
    + +
    + +
    +
    + + diff --git a/target/site/testapidocs/com/studentgui/test/BrailleSmokeTest.html b/target/site/testapidocs/com/studentgui/test/BrailleSmokeTest.html new file mode 100644 index 0000000..1b19a6c --- /dev/null +++ b/target/site/testapidocs/com/studentgui/test/BrailleSmokeTest.html @@ -0,0 +1,186 @@ + + + + +BrailleSmokeTest (Vision Skills Progression Tracker 1.0.0-beta Test API) + + + + + + + + + + + + + + +
    + +
    +
    + +
    + +

    Class BrailleSmokeTest

    +
    +
    java.lang.Object +
    com.studentgui.test.BrailleSmokeTest
    +
    +
    +
    +
    public class BrailleSmokeTest +extends Object
    +
    JUnit replacement for the legacy Braille smoke main. Exercises the + normalized database APIs and invokes JLineGraph.updateWithData(...) to + verify plumbing without launching the full GUI.
    +
    +
    + +
    +
    + +
    + +
    + +
    +
    + + diff --git a/target/site/testapidocs/com/studentgui/test/DatabaseEdgeCasesTest.html b/target/site/testapidocs/com/studentgui/test/DatabaseEdgeCasesTest.html new file mode 100644 index 0000000..9654568 --- /dev/null +++ b/target/site/testapidocs/com/studentgui/test/DatabaseEdgeCasesTest.html @@ -0,0 +1,228 @@ + + + + +DatabaseEdgeCasesTest (Vision Skills Progression Tracker 1.0.0-beta Test API) + + + + + + + + + + + + + + +
    + +
    +
    + +
    + +

    Class DatabaseEdgeCasesTest

    +
    +
    java.lang.Object +
    com.studentgui.test.DatabaseEdgeCasesTest
    +
    +
    +
    +
    public class DatabaseEdgeCasesTest +extends Object
    +
    +
    + +
    +
    + +
    + +
    + +
    +
    + + diff --git a/target/site/testapidocs/com/studentgui/test/DatabaseTest.html b/target/site/testapidocs/com/studentgui/test/DatabaseTest.html new file mode 100644 index 0000000..90f8e4e --- /dev/null +++ b/target/site/testapidocs/com/studentgui/test/DatabaseTest.html @@ -0,0 +1,201 @@ + + + + +DatabaseTest (Vision Skills Progression Tracker 1.0.0-beta Test API) + + + + + + + + + + + + + + +
    + +
    +
    + +
    + +

    Class DatabaseTest

    +
    +
    java.lang.Object +
    com.studentgui.test.DatabaseTest
    +
    +
    +
    +
    public class DatabaseTest +extends Object
    +
    Basic integration tests for the Database helper using the on-disk sqlite + created in the project's application data folder. These tests are small and + intentionally exercise CRUD paths used by the UI pages.
    +
    +
    + +
    +
    + +
    + +
    + +
    +
    + + diff --git a/target/site/testapidocs/com/studentgui/test/ExportBrailleReportsTest.html b/target/site/testapidocs/com/studentgui/test/ExportBrailleReportsTest.html new file mode 100644 index 0000000..14281cc --- /dev/null +++ b/target/site/testapidocs/com/studentgui/test/ExportBrailleReportsTest.html @@ -0,0 +1,187 @@ + + + + +ExportBrailleReportsTest (Vision Skills Progression Tracker 1.0.0-beta Test API) + + + + + + + + + + + + + + +
    + +
    +
    + +
    + +

    Class ExportBrailleReportsTest

    +
    +
    java.lang.Object +
    com.studentgui.test.ExportBrailleReportsTest
    +
    +
    +
    +
    public class ExportBrailleReportsTest +extends Object
    +
    Test that generates example Braille exports (per-phase PNGs + MD/HTML) + for the student "Test Student". This mirrors the export logic used in + the Braille page submit handler but runs headlessly as a test so the + agent can produce example files for review.
    +
    +
    + +
    +
    + +
    + +
    + +
    +
    + + diff --git a/target/site/testapidocs/com/studentgui/test/class-use/BrailleDatabaseTest.html b/target/site/testapidocs/com/studentgui/test/class-use/BrailleDatabaseTest.html new file mode 100644 index 0000000..15a70c2 --- /dev/null +++ b/target/site/testapidocs/com/studentgui/test/class-use/BrailleDatabaseTest.html @@ -0,0 +1,62 @@ + + + + +Uses of Class com.studentgui.test.BrailleDatabaseTest (Vision Skills Progression Tracker 1.0.0-beta Test API) + + + + + + + + + + + + + + +
    + +
    +
    +
    +

    Uses of Class
    com.studentgui.test.BrailleDatabaseTest

    +
    +No usage of com.studentgui.test.BrailleDatabaseTest
    + +
    +
    + + diff --git a/target/site/testapidocs/com/studentgui/test/class-use/BrailleSmokeTest.html b/target/site/testapidocs/com/studentgui/test/class-use/BrailleSmokeTest.html new file mode 100644 index 0000000..0ebcee6 --- /dev/null +++ b/target/site/testapidocs/com/studentgui/test/class-use/BrailleSmokeTest.html @@ -0,0 +1,62 @@ + + + + +Uses of Class com.studentgui.test.BrailleSmokeTest (Vision Skills Progression Tracker 1.0.0-beta Test API) + + + + + + + + + + + + + + +
    + +
    +
    +
    +

    Uses of Class
    com.studentgui.test.BrailleSmokeTest

    +
    +No usage of com.studentgui.test.BrailleSmokeTest
    + +
    +
    + + diff --git a/target/site/testapidocs/com/studentgui/test/class-use/DatabaseEdgeCasesTest.html b/target/site/testapidocs/com/studentgui/test/class-use/DatabaseEdgeCasesTest.html new file mode 100644 index 0000000..363e6f3 --- /dev/null +++ b/target/site/testapidocs/com/studentgui/test/class-use/DatabaseEdgeCasesTest.html @@ -0,0 +1,62 @@ + + + + +Uses of Class com.studentgui.test.DatabaseEdgeCasesTest (Vision Skills Progression Tracker 1.0.0-beta Test API) + + + + + + + + + + + + + + +
    + +
    +
    +
    +

    Uses of Class
    com.studentgui.test.DatabaseEdgeCasesTest

    +
    +No usage of com.studentgui.test.DatabaseEdgeCasesTest
    + +
    +
    + + diff --git a/target/site/testapidocs/com/studentgui/test/class-use/DatabaseTest.html b/target/site/testapidocs/com/studentgui/test/class-use/DatabaseTest.html new file mode 100644 index 0000000..1e1f628 --- /dev/null +++ b/target/site/testapidocs/com/studentgui/test/class-use/DatabaseTest.html @@ -0,0 +1,62 @@ + + + + +Uses of Class com.studentgui.test.DatabaseTest (Vision Skills Progression Tracker 1.0.0-beta Test API) + + + + + + + + + + + + + + +
    + +
    +
    +
    +

    Uses of Class
    com.studentgui.test.DatabaseTest

    +
    +No usage of com.studentgui.test.DatabaseTest
    + +
    +
    + + diff --git a/target/site/testapidocs/com/studentgui/test/class-use/ExportBrailleReportsTest.html b/target/site/testapidocs/com/studentgui/test/class-use/ExportBrailleReportsTest.html new file mode 100644 index 0000000..2f01424 --- /dev/null +++ b/target/site/testapidocs/com/studentgui/test/class-use/ExportBrailleReportsTest.html @@ -0,0 +1,62 @@ + + + + +Uses of Class com.studentgui.test.ExportBrailleReportsTest (Vision Skills Progression Tracker 1.0.0-beta Test API) + + + + + + + + + + + + + + +
    + +
    +
    +
    +

    Uses of Class
    com.studentgui.test.ExportBrailleReportsTest

    +
    +No usage of com.studentgui.test.ExportBrailleReportsTest
    + +
    +
    + + diff --git a/target/site/testapidocs/com/studentgui/test/package-summary.html b/target/site/testapidocs/com/studentgui/test/package-summary.html new file mode 100644 index 0000000..24b85ce --- /dev/null +++ b/target/site/testapidocs/com/studentgui/test/package-summary.html @@ -0,0 +1,116 @@ + + + + +com.studentgui.test (Vision Skills Progression Tracker 1.0.0-beta Test API) + + + + + + + + + + + + + + +
    + +
    +
    +
    +

    Package com.studentgui.test

    +
    +
    +
    package com.studentgui.test
    +
    +
      +
    • +
      +
      Classes
      +
      +
      Class
      +
      Description
      + +
      +
      Small integration-style unit test that uses the normalized Database helper methods + to create a student, a progress type, ensure parts, insert one session and fetch the + latest results.
      +
      + +
      +
      JUnit replacement for the legacy Braille smoke main.
      +
      + +
       
      + +
      +
      Basic integration tests for the Database helper using the on-disk sqlite + created in the project's application data folder.
      +
      + +
      +
      Test that generates example Braille exports (per-phase PNGs + MD/HTML) + for the student "Test Student".
      +
      +
      +
      +
    • +
    +
    +
    + +
    +
    + + diff --git a/target/site/testapidocs/com/studentgui/test/package-tree.html b/target/site/testapidocs/com/studentgui/test/package-tree.html new file mode 100644 index 0000000..126de26 --- /dev/null +++ b/target/site/testapidocs/com/studentgui/test/package-tree.html @@ -0,0 +1,80 @@ + + + + +com.studentgui.test Class Hierarchy (Vision Skills Progression Tracker 1.0.0-beta Test API) + + + + + + + + + + + + + + +
    + +
    +
    +
    +

    Hierarchy For Package com.studentgui.test

    +
    +Package Hierarchies: + +
    +

    Class Hierarchy

    + +
    +
    + +
    +
    + + diff --git a/target/site/testapidocs/com/studentgui/test/package-use.html b/target/site/testapidocs/com/studentgui/test/package-use.html new file mode 100644 index 0000000..7d86452 --- /dev/null +++ b/target/site/testapidocs/com/studentgui/test/package-use.html @@ -0,0 +1,62 @@ + + + + +Uses of Package com.studentgui.test (Vision Skills Progression Tracker 1.0.0-beta Test API) + + + + + + + + + + + + + + +
    + +
    +
    +
    +

    Uses of Package
    com.studentgui.test

    +
    +No usage of com.studentgui.test
    + +
    +
    + + diff --git a/target/site/testapidocs/copy.svg b/target/site/testapidocs/copy.svg new file mode 100644 index 0000000..7c46ab1 --- /dev/null +++ b/target/site/testapidocs/copy.svg @@ -0,0 +1,33 @@ + + + + + + + + diff --git a/target/site/testapidocs/element-list b/target/site/testapidocs/element-list new file mode 100644 index 0000000..2fe90ac --- /dev/null +++ b/target/site/testapidocs/element-list @@ -0,0 +1,3 @@ +com.studentgui.apphelpers +com.studentgui.apppages +com.studentgui.test diff --git a/target/site/testapidocs/help-doc.html b/target/site/testapidocs/help-doc.html new file mode 100644 index 0000000..59b50c4 --- /dev/null +++ b/target/site/testapidocs/help-doc.html @@ -0,0 +1,193 @@ + + + + +API Help (Vision Skills Progression Tracker 1.0.0-beta Test API) + + + + + + + + + + + + + + +
    + +
    +
    +

    JavaDoc Help

    + +
    +
    +

    Navigation

    +Starting from the Overview page, you can browse the documentation using the links in each page, and in the navigation bar at the top of each page. The Index and Search box allow you to navigate to specific declarations and summary pages, including: All Packages, All Classes and Interfaces + +
    +
    +
    +

    Kinds of Pages

    +The following sections describe the different kinds of pages in this collection. +
    +

    Overview

    +

    The Overview page is the front page of this API document and provides a list of all packages with a summary for each. This page can also contain an overall description of the set of packages.

    +
    +
    +

    Package

    +

    Each package has a page that contains a list of its classes and interfaces, with a summary for each. These pages may contain the following categories:

    +
      +
    • Interfaces
    • +
    • Classes
    • +
    • Enum Classes
    • +
    • Exception Classes
    • +
    • Annotation Interfaces
    • +
    +
    +
    +

    Class or Interface

    +

    Each class, interface, nested class and nested interface has its own separate page. Each of these pages has three sections consisting of a declaration and description, member summary tables, and detailed member descriptions. Entries in each of these sections are omitted if they are empty or not applicable.

    +
      +
    • Class Inheritance Diagram
    • +
    • Direct Subclasses
    • +
    • All Known Subinterfaces
    • +
    • All Known Implementing Classes
    • +
    • Class or Interface Declaration
    • +
    • Class or Interface Description
    • +
    +
    +
      +
    • Nested Class Summary
    • +
    • Enum Constant Summary
    • +
    • Field Summary
    • +
    • Property Summary
    • +
    • Constructor Summary
    • +
    • Method Summary
    • +
    • Required Element Summary
    • +
    • Optional Element Summary
    • +
    +
    +
      +
    • Enum Constant Details
    • +
    • Field Details
    • +
    • Property Details
    • +
    • Constructor Details
    • +
    • Method Details
    • +
    • Element Details
    • +
    +

    Note: Annotation interfaces have required and optional elements, but not methods. Only enum classes have enum constants. The components of a record class are displayed as part of the declaration of the record class. Properties are a feature of JavaFX.

    +

    The summary entries are alphabetical, while the detailed descriptions are in the order they appear in the source code. This preserves the logical groupings established by the programmer.

    +
    +
    +

    Other Files

    +

    Packages and modules may contain pages with additional information related to the declarations nearby.

    +
    +
    +

    Use

    +

    Each documented package, class and interface has its own Use page. This page describes what packages, classes, methods, constructors and fields use any part of the given class or package. Given a class or interface A, its Use page includes subclasses of A, fields declared as A, methods that return A, and methods and constructors with parameters of type A. You can access this page by first going to the package, class or interface, then clicking on the USE link in the navigation bar.

    +
    +
    +

    Tree (Class Hierarchy)

    +

    There is a Class Hierarchy page for all packages, plus a hierarchy for each package. Each hierarchy page contains a list of classes and a list of interfaces. Classes are organized by inheritance structure starting with java.lang.Object. Interfaces do not inherit from java.lang.Object.

    +
      +
    • When viewing the Overview page, clicking on TREE displays the hierarchy for all packages.
    • +
    • When viewing a particular package, class or interface page, clicking on TREE displays the hierarchy for only that package.
    • +
    +
    +
    +

    All Packages

    +

    The All Packages page contains an alphabetic index of all packages contained in the documentation.

    +
    +
    +

    All Classes and Interfaces

    +

    The All Classes and Interfaces page contains an alphabetic index of all classes and interfaces contained in the documentation, including annotation interfaces, enum classes, and record classes.

    +
    +
    +

    Index

    +

    The Index contains an alphabetic index of all classes, interfaces, constructors, methods, and fields in the documentation, as well as summary pages such as All Packages, All Classes and Interfaces.

    +
    +
    +
    +This help file applies to API documentation generated by the standard doclet.
    + +
    +
    + + diff --git a/target/site/testapidocs/index-all.html b/target/site/testapidocs/index-all.html new file mode 100644 index 0000000..de57ff7 --- /dev/null +++ b/target/site/testapidocs/index-all.html @@ -0,0 +1,177 @@ + + + + +Index (Vision Skills Progression Tracker 1.0.0-beta Test API) + + + + + + + + + + + + + + +
    + +
    +
    +
    +

    Index

    +
    +B C D E G I J S T W 
    All Classes and Interfaces|All Packages +

    B

    +
    +
    BrailleDatabaseTest - Class in com.studentgui.test
    +
    +
    Small integration-style unit test that uses the normalized Database helper methods + to create a student, a progress type, ensure parts, insert one session and fetch the + latest results.
    +
    +
    BrailleDatabaseTest() - Constructor for class com.studentgui.test.BrailleDatabaseTest
    +
     
    +
    BrailleSmokeTest - Class in com.studentgui.test
    +
    +
    JUnit replacement for the legacy Braille smoke main.
    +
    +
    BrailleSmokeTest() - Constructor for class com.studentgui.test.BrailleSmokeTest
    +
     
    +
    +

    C

    +
    +
    com.studentgui.apphelpers - package com.studentgui.apphelpers
    +
     
    +
    com.studentgui.apppages - package com.studentgui.apppages
    +
     
    +
    com.studentgui.test - package com.studentgui.test
    +
     
    +
    +

    D

    +
    +
    DatabaseContactLogTest - Class in com.studentgui.apphelpers
    +
     
    +
    DatabaseContactLogTest() - Constructor for class com.studentgui.apphelpers.DatabaseContactLogTest
    +
     
    +
    DatabaseEdgeCasesTest - Class in com.studentgui.test
    +
     
    +
    DatabaseEdgeCasesTest() - Constructor for class com.studentgui.test.DatabaseEdgeCasesTest
    +
     
    +
    DatabaseTest - Class in com.studentgui.test
    +
    +
    Basic integration tests for the Database helper using the on-disk sqlite + created in the project's application data folder.
    +
    +
    DatabaseTest() - Constructor for class com.studentgui.test.DatabaseTest
    +
     
    +
    deterministicJitterProducesSameSequence() - Method in class com.studentgui.apppages.JLineGraphDeterministicJitterTest
    +
     
    +
    duplicateStudentNamesReturnSameId() - Method in class com.studentgui.test.DatabaseEdgeCasesTest
    +
     
    +
    +

    E

    +
    +
    ensureAssessmentPartsIsIdempotentAndIgnoresUnknownPartsOnInsert() - Method in class com.studentgui.test.DatabaseEdgeCasesTest
    +
     
    +
    ExportBrailleReportsTest - Class in com.studentgui.test
    +
    +
    Test that generates example Braille exports (per-phase PNGs + MD/HTML) + for the student "Test Student".
    +
    +
    ExportBrailleReportsTest() - Constructor for class com.studentgui.test.ExportBrailleReportsTest
    +
     
    +
    +

    G

    +
    +
    generateBrailleExport() - Method in class com.studentgui.test.ExportBrailleReportsTest
    +
     
    +
    +

    I

    +
    +
    init() - Static method in class com.studentgui.test.DatabaseEdgeCasesTest
    +
     
    +
    init() - Static method in class com.studentgui.test.DatabaseTest
    +
     
    +
    +

    J

    +
    +
    JLineGraphDeterministicJitterTest - Class in com.studentgui.apppages
    +
    +
    Small unit test to validate deterministic jitter reproducibility.
    +
    +
    JLineGraphDeterministicJitterTest() - Constructor for class com.studentgui.apppages.JLineGraphDeterministicJitterTest
    +
     
    +
    +

    S

    +
    +
    saveSessionNotesPersistsNotes() - Method in class com.studentgui.test.DatabaseEdgeCasesTest
    +
     
    +
    SessionJsonWriterTest - Class in com.studentgui.apphelpers
    +
    +
    Unit test for SessionJsonWriter to verify envelope and filename format.
    +
    +
    SessionJsonWriterTest() - Constructor for class com.studentgui.apphelpers.SessionJsonWriterTest
    +
     
    +
    smokeDatabaseFlow() - Method in class com.studentgui.test.BrailleDatabaseTest
    +
     
    +
    smokeTestDatabaseAndGraph() - Method in class com.studentgui.test.BrailleSmokeTest
    +
     
    +
    SqlGenerateTest - Class in com.studentgui.apphelpers
    +
     
    +
    SqlGenerateTest() - Constructor for class com.studentgui.apphelpers.SqlGenerateTest
    +
     
    +
    +

    T

    +
    +
    testInitializeCreatesContactLogTable() - Method in class com.studentgui.apphelpers.SqlGenerateTest
    +
     
    +
    testSaveAndFetchContactLog() - Method in class com.studentgui.apphelpers.DatabaseContactLogTest
    +
     
    +
    testStudentCreateAndFetch() - Method in class com.studentgui.test.DatabaseTest
    +
     
    +
    +

    W

    +
    +
    writeSessionJson_includesSessionIdAndPayload() - Method in class com.studentgui.apphelpers.SessionJsonWriterTest
    +
     
    +
    +B C D E G I J S T W 
    All Classes and Interfaces|All Packages
    + +
    +
    + + diff --git a/target/site/testapidocs/index.html b/target/site/testapidocs/index.html new file mode 100644 index 0000000..b040bab --- /dev/null +++ b/target/site/testapidocs/index.html @@ -0,0 +1,75 @@ + + + + +Overview (Vision Skills Progression Tracker 1.0.0-beta Test API) + + + + + + + + + + + + + + +
    + +
    +
    +
    +

    Vision Skills Progression Tracker 1.0.0-beta Test API

    +
    +
    +
    Packages
    + +
    +
    + +
    +
    + + diff --git a/target/site/testapidocs/legal/ADDITIONAL_LICENSE_INFO b/target/site/testapidocs/legal/ADDITIONAL_LICENSE_INFO new file mode 100644 index 0000000..ff700cd --- /dev/null +++ b/target/site/testapidocs/legal/ADDITIONAL_LICENSE_INFO @@ -0,0 +1,37 @@ + ADDITIONAL INFORMATION ABOUT LICENSING + +Certain files distributed by Oracle America, Inc. and/or its affiliates are +subject to the following clarification and special exception to the GPLv2, +based on the GNU Project exception for its Classpath libraries, known as the +GNU Classpath Exception. + +Note that Oracle includes multiple, independent programs in this software +package. Some of those programs are provided under licenses deemed +incompatible with the GPLv2 by the Free Software Foundation and others. +For example, the package includes programs licensed under the Apache +License, Version 2.0 and may include FreeType. Such programs are licensed +to you under their original licenses. + +Oracle facilitates your further distribution of this package by adding the +Classpath Exception to the necessary parts of its GPLv2 code, which permits +you to use that code in combination with other independent modules not +licensed under the GPLv2. However, note that this would not permit you to +commingle code under an incompatible license with Oracle's GPLv2 licensed +code by, for example, cutting and pasting such code into a file also +containing Oracle's GPLv2 licensed code and then distributing the result. + +Additionally, if you were to remove the Classpath Exception from any of the +files to which it applies and distribute the result, you would likely be +required to license some or all of the other code in that distribution under +the GPLv2 as well, and since the GPLv2 is incompatible with the license terms +of some items included in the distribution by Oracle, removing the Classpath +Exception could therefore effectively compromise your ability to further +distribute the package. + +Failing to distribute notices associated with some files may also create +unexpected legal consequences. + +Proceed with caution and we recommend that you obtain the advice of a lawyer +skilled in open source matters before removing the Classpath Exception or +making modifications to this package which may subsequently be redistributed +and/or involve the use of third party software. diff --git a/target/site/testapidocs/legal/ASSEMBLY_EXCEPTION b/target/site/testapidocs/legal/ASSEMBLY_EXCEPTION new file mode 100644 index 0000000..4296666 --- /dev/null +++ b/target/site/testapidocs/legal/ASSEMBLY_EXCEPTION @@ -0,0 +1,27 @@ + +OPENJDK ASSEMBLY EXCEPTION + +The OpenJDK source code made available by Oracle America, Inc. (Oracle) at +openjdk.org ("OpenJDK Code") is distributed under the terms of the GNU +General Public License version 2 +only ("GPL2"), with the following clarification and special exception. + + Linking this OpenJDK Code statically or dynamically with other code + is making a combined work based on this library. Thus, the terms + and conditions of GPL2 cover the whole combination. + + As a special exception, Oracle gives you permission to link this + OpenJDK Code with certain code licensed by Oracle as indicated at + https://openjdk.org/legal/exception-modules-2007-05-08.html + ("Designated Exception Modules") to produce an executable, + regardless of the license terms of the Designated Exception Modules, + and to copy and distribute the resulting executable under GPL2, + provided that the Designated Exception Modules continue to be + governed by the licenses under which they were offered by Oracle. + +As such, it allows licensees and sublicensees of Oracle's GPL2 OpenJDK Code +to build an executable that includes those portions of necessary code that +Oracle could not provide under GPL2 (or that Oracle has provided under GPL2 +with the Classpath exception). If you modify or add to the OpenJDK code, +that new GPL2 code may still be combined with Designated Exception Modules +if the new code is made subject to this exception by its copyright holder. diff --git a/target/site/testapidocs/legal/LICENSE b/target/site/testapidocs/legal/LICENSE new file mode 100644 index 0000000..8b400c7 --- /dev/null +++ b/target/site/testapidocs/legal/LICENSE @@ -0,0 +1,347 @@ +The GNU General Public License (GPL) + +Version 2, June 1991 + +Copyright (C) 1989, 1991 Free Software Foundation, Inc. +51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA + +Everyone is permitted to copy and distribute verbatim copies of this license +document, but changing it is not allowed. + +Preamble + +The licenses for most software are designed to take away your freedom to share +and change it. By contrast, the GNU General Public License is intended to +guarantee your freedom to share and change free software--to make sure the +software is free for all its users. This General Public License applies to +most of the Free Software Foundation's software and to any other program whose +authors commit to using it. (Some other Free Software Foundation software is +covered by the GNU Library General Public License instead.) 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 this service 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 make restrictions that forbid anyone to deny +you these rights or to ask you to surrender the rights. These restrictions +translate to certain responsibilities for you if you distribute copies of the +software, or if you modify it. + +For example, if you distribute copies of such a program, whether gratis or for +a fee, you must give the recipients all the rights that you have. 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. + +We protect your rights with two steps: (1) copyright the software, and (2) +offer you this license which gives you legal permission to copy, distribute +and/or modify the software. + +Also, for each author's protection and ours, we want to make certain that +everyone understands that there is no warranty for this free software. If the +software is modified by someone else and passed on, we want its recipients to +know that what they have is not the original, so that any problems introduced +by others will not reflect on the original authors' reputations. + +Finally, any free program is threatened constantly by software patents. We +wish to avoid the danger that redistributors of a free program will +individually obtain patent licenses, in effect making the program proprietary. +To prevent this, we have made it clear that any patent must be licensed for +everyone's free use or not licensed at all. + +The precise terms and conditions for copying, distribution and modification +follow. + +TERMS AND CONDITIONS FOR COPYING, DISTRIBUTION AND MODIFICATION + +0. This License applies to any program or other work which contains a notice +placed by the copyright holder saying it may be distributed under the terms of +this General Public License. The "Program", below, refers to any such program +or work, and a "work based on the Program" means either the Program or any +derivative work under copyright law: that is to say, a work containing the +Program or a portion of it, either verbatim or with modifications and/or +translated into another language. (Hereinafter, translation is included +without limitation in the term "modification".) Each licensee is addressed as +"you". + +Activities other than copying, distribution and modification are not covered by +this License; they are outside its scope. The act of running the Program is +not restricted, and the output from the Program is covered only if its contents +constitute a work based on the Program (independent of having been made by +running the Program). Whether that is true depends on what the Program does. + +1. You may copy and distribute 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 and +disclaimer of warranty; keep intact all the notices that refer to this License +and to the absence of any warranty; and give any other recipients of the +Program a copy of this License along with the Program. + +You may charge a fee for the physical act of transferring a copy, and you may +at your option offer warranty protection in exchange for a fee. + +2. You may modify your copy or copies of the Program or any portion of it, thus +forming a work based on the Program, and copy and distribute such modifications +or work under the terms of Section 1 above, provided that you also meet all of +these conditions: + + a) You must cause the modified files to carry prominent notices stating + that you changed the files and the date of any change. + + b) You must cause any work that you distribute or publish, that in whole or + in part contains or is derived from the Program or any part thereof, to be + licensed as a whole at no charge to all third parties under the terms of + this License. + + c) If the modified program normally reads commands interactively when run, + you must cause it, when started running for such interactive use in the + most ordinary way, to print or display an announcement including an + appropriate copyright notice and a notice that there is no warranty (or + else, saying that you provide a warranty) and that users may redistribute + the program under these conditions, and telling the user how to view a copy + of this License. (Exception: if the Program itself is interactive but does + not normally print such an announcement, your work based on the Program is + not required to print an announcement.) + +These requirements apply to the modified work as a whole. If identifiable +sections of that work are not derived from the Program, and can be reasonably +considered independent and separate works in themselves, then this License, and +its terms, do not apply to those sections when you distribute them as separate +works. But when you distribute the same sections as part of a whole which is a +work based on the Program, the distribution of the whole must be on the terms +of this License, whose permissions for other licensees extend to the entire +whole, and thus to each and every part regardless of who wrote it. + +Thus, it is not the intent of this section to claim rights or contest your +rights to work written entirely by you; rather, the intent is to exercise the +right to control the distribution of derivative or collective works based on +the Program. + +In addition, mere aggregation of another work not based on the Program with the +Program (or with a work based on the Program) on a volume of a storage or +distribution medium does not bring the other work under the scope of this +License. + +3. You may copy and distribute the Program (or a work based on it, under +Section 2) in object code or executable form under the terms of Sections 1 and +2 above provided that you also do one of the following: + + a) Accompany it with the complete corresponding machine-readable source + code, which must be distributed under the terms of Sections 1 and 2 above + on a medium customarily used for software interchange; or, + + b) Accompany it with a written offer, valid for at least three years, to + give any third party, for a charge no more than your cost of physically + performing source distribution, a complete machine-readable copy of the + corresponding source code, to be distributed under the terms of Sections 1 + and 2 above on a medium customarily used for software interchange; or, + + c) Accompany it with the information you received as to the offer to + distribute corresponding source code. (This alternative is allowed only + for noncommercial distribution and only if you received the program in + object code or executable form with such an offer, in accord with + Subsection b above.) + +The source code for a work means the preferred form of the work for making +modifications to it. For an executable work, complete source code means all +the source code for all modules it contains, plus any associated interface +definition files, plus the scripts used to control compilation and installation +of the executable. However, as a special exception, the source code +distributed need not include anything that is normally distributed (in either +source or binary form) with the major components (compiler, kernel, and so on) +of the operating system on which the executable runs, unless that component +itself accompanies the executable. + +If distribution of executable or object code is made by offering access to copy +from a designated place, then offering equivalent access to copy the source +code from the same place counts as distribution of the source code, even though +third parties are not compelled to copy the source along with the object code. + +4. You may not copy, modify, sublicense, or distribute the Program except as +expressly provided under this License. Any attempt otherwise to copy, modify, +sublicense or distribute the Program is void, and will automatically terminate +your rights under this License. However, parties who have received copies, or +rights, from you under this License will not have their licenses terminated so +long as such parties remain in full compliance. + +5. You are not required to accept this License, since you have not signed it. +However, nothing else grants you permission to modify or distribute the Program +or its derivative works. These actions are prohibited by law if you do not +accept this License. Therefore, by modifying or distributing the Program (or +any work based on the Program), you indicate your acceptance of this License to +do so, and all its terms and conditions for copying, distributing or modifying +the Program or works based on it. + +6. Each time you redistribute the Program (or any work based on the Program), +the recipient automatically receives a license from the original licensor to +copy, distribute or modify the Program subject to these terms and conditions. +You may not impose any further restrictions on the recipients' exercise of the +rights granted herein. You are not responsible for enforcing compliance by +third parties to this License. + +7. If, as a consequence of a court judgment or allegation of patent +infringement or for any other reason (not limited to patent issues), 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 distribute so as to satisfy +simultaneously your obligations under this License and any other pertinent +obligations, then as a consequence you may not distribute the Program at all. +For example, if a patent license would not permit royalty-free redistribution +of the Program by all those who receive copies directly or indirectly through +you, then the only way you could satisfy both it and this License would be to +refrain entirely from distribution of the Program. + +If any portion of this section is held invalid or unenforceable under any +particular circumstance, the balance of the section is intended to apply and +the section as a whole is intended to apply in other circumstances. + +It is not the purpose of this section to induce you to infringe any patents or +other property right claims or to contest validity of any such claims; this +section has the sole purpose of protecting the integrity of the free software +distribution system, which is implemented by public license practices. Many +people have made generous contributions to the wide range of software +distributed through that system in reliance on consistent application of that +system; it is up to the author/donor to decide if he or she is willing to +distribute software through any other system and a licensee cannot impose that +choice. + +This section is intended to make thoroughly clear what is believed to be a +consequence of the rest of this License. + +8. If the distribution and/or use of the Program is restricted in certain +countries either by patents or by copyrighted interfaces, the original +copyright holder who places the Program under this License may add an explicit +geographical distribution limitation excluding those countries, so that +distribution is permitted only in or among countries not thus excluded. In +such case, this License incorporates the limitation as if written in the body +of this License. + +9. The Free Software Foundation may publish revised and/or new versions of the +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 a version number of this License which applies to it and "any later +version", you have the option of following the terms and conditions either of +that version or of any later version published by the Free Software Foundation. +If the Program does not specify a version number of this License, you may +choose any version ever published by the Free Software Foundation. + +10. If you wish to incorporate parts of the Program into other free programs +whose distribution conditions are different, write to the author to ask for +permission. For software which is copyrighted by the Free Software Foundation, +write to the Free Software Foundation; we sometimes make exceptions for this. +Our decision will be guided by the two goals of preserving the free status of +all derivatives of our free software and of promoting the sharing and reuse of +software generally. + +NO WARRANTY + +11. BECAUSE THE PROGRAM IS LICENSED FREE OF CHARGE, 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. + +12. IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING WILL +ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MAY MODIFY AND/OR REDISTRIBUTE 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. + +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 convey the exclusion +of warranty; and each file should have at least the "copyright" line and a +pointer to where the full notice is found. + + One line to give the program's name and a brief idea of what it does. + + Copyright (C) + + 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 2 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, write to the Free Software Foundation, Inc., + 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. + +Also add information on how to contact you by electronic and paper mail. + +If the program is interactive, make it output a short notice like this when it +starts in an interactive mode: + + Gnomovision version 69, Copyright (C) year name of author Gnomovision 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, the commands you use may be +called something other than 'show w' and 'show c'; they could even be +mouse-clicks or menu items--whatever suits your program. + +You should also get your employer (if you work as a programmer) or your school, +if any, to sign a "copyright disclaimer" for the program, if necessary. Here +is a sample; alter the names: + + Yoyodyne, Inc., hereby disclaims all copyright interest in the program + 'Gnomovision' (which makes passes at compilers) written by James Hacker. + + signature of Ty Coon, 1 April 1989 + + Ty Coon, President of Vice + +This 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 Library General Public +License instead of this License. + + +"CLASSPATH" EXCEPTION TO THE GPL + +Certain source files distributed by Oracle America and/or its affiliates are +subject to the following clarification and special exception to the GPL, but +only where Oracle has expressly included in the particular source file's header +the words "Oracle designates this particular file as subject to the "Classpath" +exception as provided by Oracle in the LICENSE file that accompanied this code." + + Linking this library statically or dynamically with other modules is making + a combined work based on this library. Thus, the terms and conditions of + the GNU General Public License cover the whole combination. + + As a special exception, the copyright holders of this library give you + permission to link this library with independent modules to produce an + executable, regardless of the license terms of these independent modules, + and to copy and distribute the resulting executable under terms of your + choice, provided that you also meet, for each linked independent module, + the terms and conditions of the license of that module. An independent + module is a module which is not derived from or based on this library. If + you modify this library, you may extend this exception to your version of + the library, but you are not obligated to do so. If you do not wish to do + so, delete this exception statement from your version. diff --git a/target/site/testapidocs/legal/jquery.md b/target/site/testapidocs/legal/jquery.md new file mode 100644 index 0000000..a763ec6 --- /dev/null +++ b/target/site/testapidocs/legal/jquery.md @@ -0,0 +1,26 @@ +## jQuery v3.7.1 + +### jQuery License +``` +jQuery v 3.7.1 +Copyright OpenJS Foundation and other contributors, https://openjsf.org/ + +Permission is hereby granted, free of charge, to any person obtaining +a copy of this software and associated documentation files (the +"Software"), to deal in the Software without restriction, including +without limitation the rights to use, copy, modify, merge, publish, +distribute, sublicense, and/or sell copies of the Software, and to +permit persons to whom the Software is furnished to do so, subject to +the following conditions: + +The above copyright notice and this permission notice shall be +included in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND +NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE +LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION +OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION +WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. +``` diff --git a/target/site/testapidocs/legal/jqueryUI.md b/target/site/testapidocs/legal/jqueryUI.md new file mode 100644 index 0000000..8bda9d7 --- /dev/null +++ b/target/site/testapidocs/legal/jqueryUI.md @@ -0,0 +1,49 @@ +## jQuery UI v1.13.2 + +### jQuery UI License +``` +Copyright jQuery Foundation and other contributors, https://jquery.org/ + +This software consists of voluntary contributions made by many +individuals. For exact contribution history, see the revision history +available at https://github.com/jquery/jquery-ui + +The following license applies to all parts of this software except as +documented below: + +==== + +Permission is hereby granted, free of charge, to any person obtaining +a copy of this software and associated documentation files (the +"Software"), to deal in the Software without restriction, including +without limitation the rights to use, copy, modify, merge, publish, +distribute, sublicense, and/or sell copies of the Software, and to +permit persons to whom the Software is furnished to do so, subject to +the following conditions: + +The above copyright notice and this permission notice shall be +included in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND +NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE +LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION +OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION +WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + +==== + +Copyright and related rights for sample code are waived via CC0. Sample +code is defined as all source code contained within the demos directory. + +CC0: http://creativecommons.org/publicdomain/zero/1.0/ + +==== + +All files located in the node_modules and external directories are +externally maintained libraries used by this software which have their +own licenses; we recommend you read them, as their terms may differ from +the terms above. + +``` diff --git a/target/site/testapidocs/link.svg b/target/site/testapidocs/link.svg new file mode 100644 index 0000000..7ccc5ed --- /dev/null +++ b/target/site/testapidocs/link.svg @@ -0,0 +1,31 @@ + + + + + + + + diff --git a/target/site/testapidocs/member-search-index.js b/target/site/testapidocs/member-search-index.js new file mode 100644 index 0000000..3734bba --- /dev/null +++ b/target/site/testapidocs/member-search-index.js @@ -0,0 +1 @@ +memberSearchIndex = [{"p":"com.studentgui.test","c":"BrailleDatabaseTest","l":"BrailleDatabaseTest()","u":"%3Cinit%3E()"},{"p":"com.studentgui.test","c":"BrailleSmokeTest","l":"BrailleSmokeTest()","u":"%3Cinit%3E()"},{"p":"com.studentgui.apphelpers","c":"DatabaseContactLogTest","l":"DatabaseContactLogTest()","u":"%3Cinit%3E()"},{"p":"com.studentgui.test","c":"DatabaseEdgeCasesTest","l":"DatabaseEdgeCasesTest()","u":"%3Cinit%3E()"},{"p":"com.studentgui.test","c":"DatabaseTest","l":"DatabaseTest()","u":"%3Cinit%3E()"},{"p":"com.studentgui.apppages","c":"JLineGraphDeterministicJitterTest","l":"deterministicJitterProducesSameSequence()"},{"p":"com.studentgui.test","c":"DatabaseEdgeCasesTest","l":"duplicateStudentNamesReturnSameId()"},{"p":"com.studentgui.test","c":"DatabaseEdgeCasesTest","l":"ensureAssessmentPartsIsIdempotentAndIgnoresUnknownPartsOnInsert()"},{"p":"com.studentgui.test","c":"ExportBrailleReportsTest","l":"ExportBrailleReportsTest()","u":"%3Cinit%3E()"},{"p":"com.studentgui.test","c":"ExportBrailleReportsTest","l":"generateBrailleExport()"},{"p":"com.studentgui.test","c":"DatabaseEdgeCasesTest","l":"init()"},{"p":"com.studentgui.test","c":"DatabaseTest","l":"init()"},{"p":"com.studentgui.apppages","c":"JLineGraphDeterministicJitterTest","l":"JLineGraphDeterministicJitterTest()","u":"%3Cinit%3E()"},{"p":"com.studentgui.test","c":"DatabaseEdgeCasesTest","l":"saveSessionNotesPersistsNotes()"},{"p":"com.studentgui.apphelpers","c":"SessionJsonWriterTest","l":"SessionJsonWriterTest()","u":"%3Cinit%3E()"},{"p":"com.studentgui.test","c":"BrailleDatabaseTest","l":"smokeDatabaseFlow()"},{"p":"com.studentgui.test","c":"BrailleSmokeTest","l":"smokeTestDatabaseAndGraph()"},{"p":"com.studentgui.apphelpers","c":"SqlGenerateTest","l":"SqlGenerateTest()","u":"%3Cinit%3E()"},{"p":"com.studentgui.apphelpers","c":"SqlGenerateTest","l":"testInitializeCreatesContactLogTable()"},{"p":"com.studentgui.apphelpers","c":"DatabaseContactLogTest","l":"testSaveAndFetchContactLog()"},{"p":"com.studentgui.test","c":"DatabaseTest","l":"testStudentCreateAndFetch()"},{"p":"com.studentgui.apphelpers","c":"SessionJsonWriterTest","l":"writeSessionJson_includesSessionIdAndPayload()"}];updateSearchResults(); \ No newline at end of file diff --git a/target/site/testapidocs/module-search-index.js b/target/site/testapidocs/module-search-index.js new file mode 100644 index 0000000..0d59754 --- /dev/null +++ b/target/site/testapidocs/module-search-index.js @@ -0,0 +1 @@ +moduleSearchIndex = [];updateSearchResults(); \ No newline at end of file diff --git a/target/site/testapidocs/overview-summary.html b/target/site/testapidocs/overview-summary.html new file mode 100644 index 0000000..99dfcad --- /dev/null +++ b/target/site/testapidocs/overview-summary.html @@ -0,0 +1,26 @@ + + + + +Vision Skills Progression Tracker 1.0.0-beta Test API + + + + + + + + + + + +
    + +

    index.html

    +
    + + diff --git a/target/site/testapidocs/overview-tree.html b/target/site/testapidocs/overview-tree.html new file mode 100644 index 0000000..e20d122 --- /dev/null +++ b/target/site/testapidocs/overview-tree.html @@ -0,0 +1,86 @@ + + + + +Class Hierarchy (Vision Skills Progression Tracker 1.0.0-beta Test API) + + + + + + + + + + + + + + +
    + +
    +
    +
    +

    Hierarchy For All Packages

    +
    +Package Hierarchies: + +
    +

    Class Hierarchy

    + +
    +
    + +
    +
    + + diff --git a/target/site/testapidocs/package-search-index.js b/target/site/testapidocs/package-search-index.js new file mode 100644 index 0000000..c92ae1d --- /dev/null +++ b/target/site/testapidocs/package-search-index.js @@ -0,0 +1 @@ +packageSearchIndex = [{"l":"All Packages","u":"allpackages-index.html"},{"l":"com.studentgui.apphelpers"},{"l":"com.studentgui.apppages"},{"l":"com.studentgui.test"}];updateSearchResults(); \ No newline at end of file diff --git a/target/site/testapidocs/resources/glass.png b/target/site/testapidocs/resources/glass.png new file mode 100644 index 0000000000000000000000000000000000000000..a7f591f467a1c0c949bbc510156a0c1afb860a6e GIT binary patch literal 499 zcmVJoRsvExf%rEN>jUL}qZ_~k#FbE+Q;{`;0FZwVNX2n-^JoI; zP;4#$8DIy*Yk-P>VN(DUKmPse7mx+ExD4O|;?E5D0Z5($mjO3`*anwQU^s{ZDK#Lz zj>~{qyaIx5K!t%=G&2IJNzg!ChRpyLkO7}Ry!QaotAHAMpbB3AF(}|_f!G-oI|uK6 z`id_dumai5K%C3Y$;tKS_iqMPHg<*|-@e`liWLAggVM!zAP#@l;=c>S03;{#04Z~5 zN_+ss=Yg6*hTr59mzMwZ@+l~q!+?ft!fF66AXT#wWavHt30bZWFCK%!BNk}LN?0Hg z1VF_nfs`Lm^DjYZ1(1uD0u4CSIr)XAaqW6IT{!St5~1{i=i}zAy76p%_|w8rh@@c0Axr!ns=D-X+|*sY6!@wacG9%)Qn*O zl0sa739kT-&_?#oVxXF6tOnqTD)cZ}2vi$`ZU8RLAlo8=_z#*P3xI~i!lEh+Pdu-L zx{d*wgjtXbnGX_Yf@Tc7Q3YhLhPvc8noGJs2DA~1DySiA&6V{5JzFt ojAY1KXm~va;tU{v7C?Xj0BHw!K;2aXV*mgE07*qoM6N<$f;4TDA^-pY literal 0 HcmV?d00001 diff --git a/target/site/testapidocs/script-dir/jquery-3.7.1.min.js b/target/site/testapidocs/script-dir/jquery-3.7.1.min.js new file mode 100644 index 0000000..7f37b5d --- /dev/null +++ b/target/site/testapidocs/script-dir/jquery-3.7.1.min.js @@ -0,0 +1,2 @@ +/*! jQuery v3.7.1 | (c) OpenJS Foundation and other contributors | jquery.org/license */ +!function(e,t){"use strict";"object"==typeof module&&"object"==typeof module.exports?module.exports=e.document?t(e,!0):function(e){if(!e.document)throw new Error("jQuery requires a window with a document");return t(e)}:t(e)}("undefined"!=typeof window?window:this,function(ie,e){"use strict";var oe=[],r=Object.getPrototypeOf,ae=oe.slice,g=oe.flat?function(e){return oe.flat.call(e)}:function(e){return oe.concat.apply([],e)},s=oe.push,se=oe.indexOf,n={},i=n.toString,ue=n.hasOwnProperty,o=ue.toString,a=o.call(Object),le={},v=function(e){return"function"==typeof e&&"number"!=typeof e.nodeType&&"function"!=typeof e.item},y=function(e){return null!=e&&e===e.window},C=ie.document,u={type:!0,src:!0,nonce:!0,noModule:!0};function m(e,t,n){var r,i,o=(n=n||C).createElement("script");if(o.text=e,t)for(r in u)(i=t[r]||t.getAttribute&&t.getAttribute(r))&&o.setAttribute(r,i);n.head.appendChild(o).parentNode.removeChild(o)}function x(e){return null==e?e+"":"object"==typeof e||"function"==typeof e?n[i.call(e)]||"object":typeof e}var t="3.7.1",l=/HTML$/i,ce=function(e,t){return new ce.fn.init(e,t)};function c(e){var t=!!e&&"length"in e&&e.length,n=x(e);return!v(e)&&!y(e)&&("array"===n||0===t||"number"==typeof t&&0+~]|"+ge+")"+ge+"*"),x=new RegExp(ge+"|>"),j=new RegExp(g),A=new RegExp("^"+t+"$"),D={ID:new RegExp("^#("+t+")"),CLASS:new RegExp("^\\.("+t+")"),TAG:new RegExp("^("+t+"|[*])"),ATTR:new RegExp("^"+p),PSEUDO:new RegExp("^"+g),CHILD:new RegExp("^:(only|first|last|nth|nth-last)-(child|of-type)(?:\\("+ge+"*(even|odd|(([+-]|)(\\d*)n|)"+ge+"*(?:([+-]|)"+ge+"*(\\d+)|))"+ge+"*\\)|)","i"),bool:new RegExp("^(?:"+f+")$","i"),needsContext:new RegExp("^"+ge+"*[>+~]|:(even|odd|eq|gt|lt|nth|first|last)(?:\\("+ge+"*((?:-\\d)?\\d*)"+ge+"*\\)|)(?=[^-]|$)","i")},N=/^(?:input|select|textarea|button)$/i,q=/^h\d$/i,L=/^(?:#([\w-]+)|(\w+)|\.([\w-]+))$/,H=/[+~]/,O=new RegExp("\\\\[\\da-fA-F]{1,6}"+ge+"?|\\\\([^\\r\\n\\f])","g"),P=function(e,t){var n="0x"+e.slice(1)-65536;return t||(n<0?String.fromCharCode(n+65536):String.fromCharCode(n>>10|55296,1023&n|56320))},M=function(){V()},R=J(function(e){return!0===e.disabled&&fe(e,"fieldset")},{dir:"parentNode",next:"legend"});try{k.apply(oe=ae.call(ye.childNodes),ye.childNodes),oe[ye.childNodes.length].nodeType}catch(e){k={apply:function(e,t){me.apply(e,ae.call(t))},call:function(e){me.apply(e,ae.call(arguments,1))}}}function I(t,e,n,r){var i,o,a,s,u,l,c,f=e&&e.ownerDocument,p=e?e.nodeType:9;if(n=n||[],"string"!=typeof t||!t||1!==p&&9!==p&&11!==p)return n;if(!r&&(V(e),e=e||T,C)){if(11!==p&&(u=L.exec(t)))if(i=u[1]){if(9===p){if(!(a=e.getElementById(i)))return n;if(a.id===i)return k.call(n,a),n}else if(f&&(a=f.getElementById(i))&&I.contains(e,a)&&a.id===i)return k.call(n,a),n}else{if(u[2])return k.apply(n,e.getElementsByTagName(t)),n;if((i=u[3])&&e.getElementsByClassName)return k.apply(n,e.getElementsByClassName(i)),n}if(!(h[t+" "]||d&&d.test(t))){if(c=t,f=e,1===p&&(x.test(t)||m.test(t))){(f=H.test(t)&&U(e.parentNode)||e)==e&&le.scope||((s=e.getAttribute("id"))?s=ce.escapeSelector(s):e.setAttribute("id",s=S)),o=(l=Y(t)).length;while(o--)l[o]=(s?"#"+s:":scope")+" "+Q(l[o]);c=l.join(",")}try{return k.apply(n,f.querySelectorAll(c)),n}catch(e){h(t,!0)}finally{s===S&&e.removeAttribute("id")}}}return re(t.replace(ve,"$1"),e,n,r)}function W(){var r=[];return function e(t,n){return r.push(t+" ")>b.cacheLength&&delete e[r.shift()],e[t+" "]=n}}function F(e){return e[S]=!0,e}function $(e){var t=T.createElement("fieldset");try{return!!e(t)}catch(e){return!1}finally{t.parentNode&&t.parentNode.removeChild(t),t=null}}function B(t){return function(e){return fe(e,"input")&&e.type===t}}function _(t){return function(e){return(fe(e,"input")||fe(e,"button"))&&e.type===t}}function z(t){return function(e){return"form"in e?e.parentNode&&!1===e.disabled?"label"in e?"label"in e.parentNode?e.parentNode.disabled===t:e.disabled===t:e.isDisabled===t||e.isDisabled!==!t&&R(e)===t:e.disabled===t:"label"in e&&e.disabled===t}}function X(a){return F(function(o){return o=+o,F(function(e,t){var n,r=a([],e.length,o),i=r.length;while(i--)e[n=r[i]]&&(e[n]=!(t[n]=e[n]))})})}function U(e){return e&&"undefined"!=typeof e.getElementsByTagName&&e}function V(e){var t,n=e?e.ownerDocument||e:ye;return n!=T&&9===n.nodeType&&n.documentElement&&(r=(T=n).documentElement,C=!ce.isXMLDoc(T),i=r.matches||r.webkitMatchesSelector||r.msMatchesSelector,r.msMatchesSelector&&ye!=T&&(t=T.defaultView)&&t.top!==t&&t.addEventListener("unload",M),le.getById=$(function(e){return r.appendChild(e).id=ce.expando,!T.getElementsByName||!T.getElementsByName(ce.expando).length}),le.disconnectedMatch=$(function(e){return i.call(e,"*")}),le.scope=$(function(){return T.querySelectorAll(":scope")}),le.cssHas=$(function(){try{return T.querySelector(":has(*,:jqfake)"),!1}catch(e){return!0}}),le.getById?(b.filter.ID=function(e){var t=e.replace(O,P);return function(e){return e.getAttribute("id")===t}},b.find.ID=function(e,t){if("undefined"!=typeof t.getElementById&&C){var n=t.getElementById(e);return n?[n]:[]}}):(b.filter.ID=function(e){var n=e.replace(O,P);return function(e){var t="undefined"!=typeof e.getAttributeNode&&e.getAttributeNode("id");return t&&t.value===n}},b.find.ID=function(e,t){if("undefined"!=typeof t.getElementById&&C){var n,r,i,o=t.getElementById(e);if(o){if((n=o.getAttributeNode("id"))&&n.value===e)return[o];i=t.getElementsByName(e),r=0;while(o=i[r++])if((n=o.getAttributeNode("id"))&&n.value===e)return[o]}return[]}}),b.find.TAG=function(e,t){return"undefined"!=typeof t.getElementsByTagName?t.getElementsByTagName(e):t.querySelectorAll(e)},b.find.CLASS=function(e,t){if("undefined"!=typeof t.getElementsByClassName&&C)return t.getElementsByClassName(e)},d=[],$(function(e){var t;r.appendChild(e).innerHTML="",e.querySelectorAll("[selected]").length||d.push("\\["+ge+"*(?:value|"+f+")"),e.querySelectorAll("[id~="+S+"-]").length||d.push("~="),e.querySelectorAll("a#"+S+"+*").length||d.push(".#.+[+~]"),e.querySelectorAll(":checked").length||d.push(":checked"),(t=T.createElement("input")).setAttribute("type","hidden"),e.appendChild(t).setAttribute("name","D"),r.appendChild(e).disabled=!0,2!==e.querySelectorAll(":disabled").length&&d.push(":enabled",":disabled"),(t=T.createElement("input")).setAttribute("name",""),e.appendChild(t),e.querySelectorAll("[name='']").length||d.push("\\["+ge+"*name"+ge+"*="+ge+"*(?:''|\"\")")}),le.cssHas||d.push(":has"),d=d.length&&new RegExp(d.join("|")),l=function(e,t){if(e===t)return a=!0,0;var n=!e.compareDocumentPosition-!t.compareDocumentPosition;return n||(1&(n=(e.ownerDocument||e)==(t.ownerDocument||t)?e.compareDocumentPosition(t):1)||!le.sortDetached&&t.compareDocumentPosition(e)===n?e===T||e.ownerDocument==ye&&I.contains(ye,e)?-1:t===T||t.ownerDocument==ye&&I.contains(ye,t)?1:o?se.call(o,e)-se.call(o,t):0:4&n?-1:1)}),T}for(e in I.matches=function(e,t){return I(e,null,null,t)},I.matchesSelector=function(e,t){if(V(e),C&&!h[t+" "]&&(!d||!d.test(t)))try{var n=i.call(e,t);if(n||le.disconnectedMatch||e.document&&11!==e.document.nodeType)return n}catch(e){h(t,!0)}return 0":{dir:"parentNode",first:!0}," ":{dir:"parentNode"},"+":{dir:"previousSibling",first:!0},"~":{dir:"previousSibling"}},preFilter:{ATTR:function(e){return e[1]=e[1].replace(O,P),e[3]=(e[3]||e[4]||e[5]||"").replace(O,P),"~="===e[2]&&(e[3]=" "+e[3]+" "),e.slice(0,4)},CHILD:function(e){return e[1]=e[1].toLowerCase(),"nth"===e[1].slice(0,3)?(e[3]||I.error(e[0]),e[4]=+(e[4]?e[5]+(e[6]||1):2*("even"===e[3]||"odd"===e[3])),e[5]=+(e[7]+e[8]||"odd"===e[3])):e[3]&&I.error(e[0]),e},PSEUDO:function(e){var t,n=!e[6]&&e[2];return D.CHILD.test(e[0])?null:(e[3]?e[2]=e[4]||e[5]||"":n&&j.test(n)&&(t=Y(n,!0))&&(t=n.indexOf(")",n.length-t)-n.length)&&(e[0]=e[0].slice(0,t),e[2]=n.slice(0,t)),e.slice(0,3))}},filter:{TAG:function(e){var t=e.replace(O,P).toLowerCase();return"*"===e?function(){return!0}:function(e){return fe(e,t)}},CLASS:function(e){var t=s[e+" "];return t||(t=new RegExp("(^|"+ge+")"+e+"("+ge+"|$)"))&&s(e,function(e){return t.test("string"==typeof e.className&&e.className||"undefined"!=typeof e.getAttribute&&e.getAttribute("class")||"")})},ATTR:function(n,r,i){return function(e){var t=I.attr(e,n);return null==t?"!="===r:!r||(t+="","="===r?t===i:"!="===r?t!==i:"^="===r?i&&0===t.indexOf(i):"*="===r?i&&-1:\x20\t\r\n\f]*)[\x20\t\r\n\f]*\/?>(?:<\/\1>|)$/i;function T(e,n,r){return v(n)?ce.grep(e,function(e,t){return!!n.call(e,t,e)!==r}):n.nodeType?ce.grep(e,function(e){return e===n!==r}):"string"!=typeof n?ce.grep(e,function(e){return-1)[^>]*|#([\w-]+))$/;(ce.fn.init=function(e,t,n){var r,i;if(!e)return this;if(n=n||k,"string"==typeof e){if(!(r="<"===e[0]&&">"===e[e.length-1]&&3<=e.length?[null,e,null]:S.exec(e))||!r[1]&&t)return!t||t.jquery?(t||n).find(e):this.constructor(t).find(e);if(r[1]){if(t=t instanceof ce?t[0]:t,ce.merge(this,ce.parseHTML(r[1],t&&t.nodeType?t.ownerDocument||t:C,!0)),w.test(r[1])&&ce.isPlainObject(t))for(r in t)v(this[r])?this[r](t[r]):this.attr(r,t[r]);return this}return(i=C.getElementById(r[2]))&&(this[0]=i,this.length=1),this}return e.nodeType?(this[0]=e,this.length=1,this):v(e)?void 0!==n.ready?n.ready(e):e(ce):ce.makeArray(e,this)}).prototype=ce.fn,k=ce(C);var E=/^(?:parents|prev(?:Until|All))/,j={children:!0,contents:!0,next:!0,prev:!0};function A(e,t){while((e=e[t])&&1!==e.nodeType);return e}ce.fn.extend({has:function(e){var t=ce(e,this),n=t.length;return this.filter(function(){for(var e=0;e\x20\t\r\n\f]*)/i,Ce=/^$|^module$|\/(?:java|ecma)script/i;xe=C.createDocumentFragment().appendChild(C.createElement("div")),(be=C.createElement("input")).setAttribute("type","radio"),be.setAttribute("checked","checked"),be.setAttribute("name","t"),xe.appendChild(be),le.checkClone=xe.cloneNode(!0).cloneNode(!0).lastChild.checked,xe.innerHTML="",le.noCloneChecked=!!xe.cloneNode(!0).lastChild.defaultValue,xe.innerHTML="",le.option=!!xe.lastChild;var ke={thead:[1,"","
    "],col:[2,"","
    "],tr:[2,"","
    "],td:[3,"","
    "],_default:[0,"",""]};function Se(e,t){var n;return n="undefined"!=typeof e.getElementsByTagName?e.getElementsByTagName(t||"*"):"undefined"!=typeof e.querySelectorAll?e.querySelectorAll(t||"*"):[],void 0===t||t&&fe(e,t)?ce.merge([e],n):n}function Ee(e,t){for(var n=0,r=e.length;n",""]);var je=/<|&#?\w+;/;function Ae(e,t,n,r,i){for(var o,a,s,u,l,c,f=t.createDocumentFragment(),p=[],d=0,h=e.length;d\s*$/g;function Re(e,t){return fe(e,"table")&&fe(11!==t.nodeType?t:t.firstChild,"tr")&&ce(e).children("tbody")[0]||e}function Ie(e){return e.type=(null!==e.getAttribute("type"))+"/"+e.type,e}function We(e){return"true/"===(e.type||"").slice(0,5)?e.type=e.type.slice(5):e.removeAttribute("type"),e}function Fe(e,t){var n,r,i,o,a,s;if(1===t.nodeType){if(_.hasData(e)&&(s=_.get(e).events))for(i in _.remove(t,"handle events"),s)for(n=0,r=s[i].length;n").attr(n.scriptAttrs||{}).prop({charset:n.scriptCharset,src:n.url}).on("load error",i=function(e){r.remove(),i=null,e&&t("error"===e.type?404:200,e.type)}),C.head.appendChild(r[0])},abort:function(){i&&i()}}});var Jt,Kt=[],Zt=/(=)\?(?=&|$)|\?\?/;ce.ajaxSetup({jsonp:"callback",jsonpCallback:function(){var e=Kt.pop()||ce.expando+"_"+jt.guid++;return this[e]=!0,e}}),ce.ajaxPrefilter("json jsonp",function(e,t,n){var r,i,o,a=!1!==e.jsonp&&(Zt.test(e.url)?"url":"string"==typeof e.data&&0===(e.contentType||"").indexOf("application/x-www-form-urlencoded")&&Zt.test(e.data)&&"data");if(a||"jsonp"===e.dataTypes[0])return r=e.jsonpCallback=v(e.jsonpCallback)?e.jsonpCallback():e.jsonpCallback,a?e[a]=e[a].replace(Zt,"$1"+r):!1!==e.jsonp&&(e.url+=(At.test(e.url)?"&":"?")+e.jsonp+"="+r),e.converters["script json"]=function(){return o||ce.error(r+" was not called"),o[0]},e.dataTypes[0]="json",i=ie[r],ie[r]=function(){o=arguments},n.always(function(){void 0===i?ce(ie).removeProp(r):ie[r]=i,e[r]&&(e.jsonpCallback=t.jsonpCallback,Kt.push(r)),o&&v(i)&&i(o[0]),o=i=void 0}),"script"}),le.createHTMLDocument=((Jt=C.implementation.createHTMLDocument("").body).innerHTML="
    ",2===Jt.childNodes.length),ce.parseHTML=function(e,t,n){return"string"!=typeof e?[]:("boolean"==typeof t&&(n=t,t=!1),t||(le.createHTMLDocument?((r=(t=C.implementation.createHTMLDocument("")).createElement("base")).href=C.location.href,t.head.appendChild(r)):t=C),o=!n&&[],(i=w.exec(e))?[t.createElement(i[1])]:(i=Ae([e],t,o),o&&o.length&&ce(o).remove(),ce.merge([],i.childNodes)));var r,i,o},ce.fn.load=function(e,t,n){var r,i,o,a=this,s=e.indexOf(" ");return-1").append(ce.parseHTML(e)).find(r):e)}).always(n&&function(e,t){a.each(function(){n.apply(this,o||[e.responseText,t,e])})}),this},ce.expr.pseudos.animated=function(t){return ce.grep(ce.timers,function(e){return t===e.elem}).length},ce.offset={setOffset:function(e,t,n){var r,i,o,a,s,u,l=ce.css(e,"position"),c=ce(e),f={};"static"===l&&(e.style.position="relative"),s=c.offset(),o=ce.css(e,"top"),u=ce.css(e,"left"),("absolute"===l||"fixed"===l)&&-1<(o+u).indexOf("auto")?(a=(r=c.position()).top,i=r.left):(a=parseFloat(o)||0,i=parseFloat(u)||0),v(t)&&(t=t.call(e,n,ce.extend({},s))),null!=t.top&&(f.top=t.top-s.top+a),null!=t.left&&(f.left=t.left-s.left+i),"using"in t?t.using.call(e,f):c.css(f)}},ce.fn.extend({offset:function(t){if(arguments.length)return void 0===t?this:this.each(function(e){ce.offset.setOffset(this,t,e)});var e,n,r=this[0];return r?r.getClientRects().length?(e=r.getBoundingClientRect(),n=r.ownerDocument.defaultView,{top:e.top+n.pageYOffset,left:e.left+n.pageXOffset}):{top:0,left:0}:void 0},position:function(){if(this[0]){var e,t,n,r=this[0],i={top:0,left:0};if("fixed"===ce.css(r,"position"))t=r.getBoundingClientRect();else{t=this.offset(),n=r.ownerDocument,e=r.offsetParent||n.documentElement;while(e&&(e===n.body||e===n.documentElement)&&"static"===ce.css(e,"position"))e=e.parentNode;e&&e!==r&&1===e.nodeType&&((i=ce(e).offset()).top+=ce.css(e,"borderTopWidth",!0),i.left+=ce.css(e,"borderLeftWidth",!0))}return{top:t.top-i.top-ce.css(r,"marginTop",!0),left:t.left-i.left-ce.css(r,"marginLeft",!0)}}},offsetParent:function(){return this.map(function(){var e=this.offsetParent;while(e&&"static"===ce.css(e,"position"))e=e.offsetParent;return e||J})}}),ce.each({scrollLeft:"pageXOffset",scrollTop:"pageYOffset"},function(t,i){var o="pageYOffset"===i;ce.fn[t]=function(e){return M(this,function(e,t,n){var r;if(y(e)?r=e:9===e.nodeType&&(r=e.defaultView),void 0===n)return r?r[i]:e[t];r?r.scrollTo(o?r.pageXOffset:n,o?n:r.pageYOffset):e[t]=n},t,e,arguments.length)}}),ce.each(["top","left"],function(e,n){ce.cssHooks[n]=Ye(le.pixelPosition,function(e,t){if(t)return t=Ge(e,n),_e.test(t)?ce(e).position()[n]+"px":t})}),ce.each({Height:"height",Width:"width"},function(a,s){ce.each({padding:"inner"+a,content:s,"":"outer"+a},function(r,o){ce.fn[o]=function(e,t){var n=arguments.length&&(r||"boolean"!=typeof e),i=r||(!0===e||!0===t?"margin":"border");return M(this,function(e,t,n){var r;return y(e)?0===o.indexOf("outer")?e["inner"+a]:e.document.documentElement["client"+a]:9===e.nodeType?(r=e.documentElement,Math.max(e.body["scroll"+a],r["scroll"+a],e.body["offset"+a],r["offset"+a],r["client"+a])):void 0===n?ce.css(e,t,i):ce.style(e,t,n,i)},s,n?e:void 0,n)}})}),ce.each(["ajaxStart","ajaxStop","ajaxComplete","ajaxError","ajaxSuccess","ajaxSend"],function(e,t){ce.fn[t]=function(e){return this.on(t,e)}}),ce.fn.extend({bind:function(e,t,n){return this.on(e,null,t,n)},unbind:function(e,t){return this.off(e,null,t)},delegate:function(e,t,n,r){return this.on(t,e,n,r)},undelegate:function(e,t,n){return 1===arguments.length?this.off(e,"**"):this.off(t,e||"**",n)},hover:function(e,t){return this.on("mouseenter",e).on("mouseleave",t||e)}}),ce.each("blur focus focusin focusout resize scroll click dblclick mousedown mouseup mousemove mouseover mouseout mouseenter mouseleave change select submit keydown keypress keyup contextmenu".split(" "),function(e,n){ce.fn[n]=function(e,t){return 0",options:{classes:{},disabled:!1,create:null},_createWidget:function(t,e){e=x(e||this.defaultElement||this)[0],this.element=x(e),this.uuid=i++,this.eventNamespace="."+this.widgetName+this.uuid,this.bindings=x(),this.hoverable=x(),this.focusable=x(),this.classesElementLookup={},e!==this&&(x.data(e,this.widgetFullName,this),this._on(!0,this.element,{remove:function(t){t.target===e&&this.destroy()}}),this.document=x(e.style?e.ownerDocument:e.document||e),this.window=x(this.document[0].defaultView||this.document[0].parentWindow)),this.options=x.widget.extend({},this.options,this._getCreateOptions(),t),this._create(),this.options.disabled&&this._setOptionDisabled(this.options.disabled),this._trigger("create",null,this._getCreateEventData()),this._init()},_getCreateOptions:function(){return{}},_getCreateEventData:x.noop,_create:x.noop,_init:x.noop,destroy:function(){var i=this;this._destroy(),x.each(this.classesElementLookup,function(t,e){i._removeClass(e,t)}),this.element.off(this.eventNamespace).removeData(this.widgetFullName),this.widget().off(this.eventNamespace).removeAttr("aria-disabled"),this.bindings.off(this.eventNamespace)},_destroy:x.noop,widget:function(){return this.element},option:function(t,e){var i,s,n,o=t;if(0===arguments.length)return x.widget.extend({},this.options);if("string"==typeof t)if(o={},t=(i=t.split(".")).shift(),i.length){for(s=o[t]=x.widget.extend({},this.options[t]),n=0;n
  • "),i=e.children()[0];return x("body").append(e),t=i.offsetWidth,e.css("overflow","scroll"),t===(i=i.offsetWidth)&&(i=e[0].clientWidth),e.remove(),s=t-i},getScrollInfo:function(t){var e=t.isWindow||t.isDocument?"":t.element.css("overflow-x"),i=t.isWindow||t.isDocument?"":t.element.css("overflow-y"),e="scroll"===e||"auto"===e&&t.widthC(E(s),E(n))?o.important="horizontal":o.important="vertical",c.using.call(this,t,o)}),l.offset(x.extend(u,{using:t}))})},x.ui.position={fit:{left:function(t,e){var i=e.within,s=i.isWindow?i.scrollLeft:i.offset.left,n=i.width,o=t.left-e.collisionPosition.marginLeft,l=s-o,a=o+e.collisionWidth-n-s;e.collisionWidth>n?0n?0",delay:300,options:{icons:{submenu:"ui-icon-caret-1-e"},items:"> *",menus:"ul",position:{my:"left top",at:"right top"},role:"menu",blur:null,focus:null,select:null},_create:function(){this.activeMenu=this.element,this.mouseHandled=!1,this.lastMousePosition={x:null,y:null},this.element.uniqueId().attr({role:this.options.role,tabIndex:0}),this._addClass("ui-menu","ui-widget ui-widget-content"),this._on({"mousedown .ui-menu-item":function(t){t.preventDefault(),this._activateItem(t)},"click .ui-menu-item":function(t){var e=x(t.target),i=x(x.ui.safeActiveElement(this.document[0]));!this.mouseHandled&&e.not(".ui-state-disabled").length&&(this.select(t),t.isPropagationStopped()||(this.mouseHandled=!0),e.has(".ui-menu").length?this.expand(t):!this.element.is(":focus")&&i.closest(".ui-menu").length&&(this.element.trigger("focus",[!0]),this.active&&1===this.active.parents(".ui-menu").length&&clearTimeout(this.timer)))},"mouseenter .ui-menu-item":"_activateItem","mousemove .ui-menu-item":"_activateItem",mouseleave:"collapseAll","mouseleave .ui-menu":"collapseAll",focus:function(t,e){var i=this.active||this._menuItems().first();e||this.focus(t,i)},blur:function(t){this._delay(function(){x.contains(this.element[0],x.ui.safeActiveElement(this.document[0]))||this.collapseAll(t)})},keydown:"_keydown"}),this.refresh(),this._on(this.document,{click:function(t){this._closeOnDocumentClick(t)&&this.collapseAll(t,!0),this.mouseHandled=!1}})},_activateItem:function(t){var e,i;this.previousFilter||t.clientX===this.lastMousePosition.x&&t.clientY===this.lastMousePosition.y||(this.lastMousePosition={x:t.clientX,y:t.clientY},e=x(t.target).closest(".ui-menu-item"),i=x(t.currentTarget),e[0]===i[0]&&(i.is(".ui-state-active")||(this._removeClass(i.siblings().children(".ui-state-active"),null,"ui-state-active"),this.focus(t,i))))},_destroy:function(){var t=this.element.find(".ui-menu-item").removeAttr("role aria-disabled").children(".ui-menu-item-wrapper").removeUniqueId().removeAttr("tabIndex role aria-haspopup");this.element.removeAttr("aria-activedescendant").find(".ui-menu").addBack().removeAttr("role aria-labelledby aria-expanded aria-hidden aria-disabled tabIndex").removeUniqueId().show(),t.children().each(function(){var t=x(this);t.data("ui-menu-submenu-caret")&&t.remove()})},_keydown:function(t){var e,i,s,n=!0;switch(t.keyCode){case x.ui.keyCode.PAGE_UP:this.previousPage(t);break;case x.ui.keyCode.PAGE_DOWN:this.nextPage(t);break;case x.ui.keyCode.HOME:this._move("first","first",t);break;case x.ui.keyCode.END:this._move("last","last",t);break;case x.ui.keyCode.UP:this.previous(t);break;case x.ui.keyCode.DOWN:this.next(t);break;case x.ui.keyCode.LEFT:this.collapse(t);break;case x.ui.keyCode.RIGHT:this.active&&!this.active.is(".ui-state-disabled")&&this.expand(t);break;case x.ui.keyCode.ENTER:case x.ui.keyCode.SPACE:this._activate(t);break;case x.ui.keyCode.ESCAPE:this.collapse(t);break;default:e=this.previousFilter||"",s=n=!1,i=96<=t.keyCode&&t.keyCode<=105?(t.keyCode-96).toString():String.fromCharCode(t.keyCode),clearTimeout(this.filterTimer),i===e?s=!0:i=e+i,e=this._filterMenuItems(i),(e=s&&-1!==e.index(this.active.next())?this.active.nextAll(".ui-menu-item"):e).length||(i=String.fromCharCode(t.keyCode),e=this._filterMenuItems(i)),e.length?(this.focus(t,e),this.previousFilter=i,this.filterTimer=this._delay(function(){delete this.previousFilter},1e3)):delete this.previousFilter}n&&t.preventDefault()},_activate:function(t){this.active&&!this.active.is(".ui-state-disabled")&&(this.active.children("[aria-haspopup='true']").length?this.expand(t):this.select(t))},refresh:function(){var t,e,s=this,n=this.options.icons.submenu,i=this.element.find(this.options.menus);this._toggleClass("ui-menu-icons",null,!!this.element.find(".ui-icon").length),e=i.filter(":not(.ui-menu)").hide().attr({role:this.options.role,"aria-hidden":"true","aria-expanded":"false"}).each(function(){var t=x(this),e=t.prev(),i=x("").data("ui-menu-submenu-caret",!0);s._addClass(i,"ui-menu-icon","ui-icon "+n),e.attr("aria-haspopup","true").prepend(i),t.attr("aria-labelledby",e.attr("id"))}),this._addClass(e,"ui-menu","ui-widget ui-widget-content ui-front"),(t=i.add(this.element).find(this.options.items)).not(".ui-menu-item").each(function(){var t=x(this);s._isDivider(t)&&s._addClass(t,"ui-menu-divider","ui-widget-content")}),i=(e=t.not(".ui-menu-item, .ui-menu-divider")).children().not(".ui-menu").uniqueId().attr({tabIndex:-1,role:this._itemRole()}),this._addClass(e,"ui-menu-item")._addClass(i,"ui-menu-item-wrapper"),t.filter(".ui-state-disabled").attr("aria-disabled","true"),this.active&&!x.contains(this.element[0],this.active[0])&&this.blur()},_itemRole:function(){return{menu:"menuitem",listbox:"option"}[this.options.role]},_setOption:function(t,e){var i;"icons"===t&&(i=this.element.find(".ui-menu-icon"),this._removeClass(i,null,this.options.icons.submenu)._addClass(i,null,e.submenu)),this._super(t,e)},_setOptionDisabled:function(t){this._super(t),this.element.attr("aria-disabled",String(t)),this._toggleClass(null,"ui-state-disabled",!!t)},focus:function(t,e){var i;this.blur(t,t&&"focus"===t.type),this._scrollIntoView(e),this.active=e.first(),i=this.active.children(".ui-menu-item-wrapper"),this._addClass(i,null,"ui-state-active"),this.options.role&&this.element.attr("aria-activedescendant",i.attr("id")),i=this.active.parent().closest(".ui-menu-item").children(".ui-menu-item-wrapper"),this._addClass(i,null,"ui-state-active"),t&&"keydown"===t.type?this._close():this.timer=this._delay(function(){this._close()},this.delay),(i=e.children(".ui-menu")).length&&t&&/^mouse/.test(t.type)&&this._startOpening(i),this.activeMenu=e.parent(),this._trigger("focus",t,{item:e})},_scrollIntoView:function(t){var e,i,s;this._hasScroll()&&(i=parseFloat(x.css(this.activeMenu[0],"borderTopWidth"))||0,s=parseFloat(x.css(this.activeMenu[0],"paddingTop"))||0,e=t.offset().top-this.activeMenu.offset().top-i-s,i=this.activeMenu.scrollTop(),s=this.activeMenu.height(),t=t.outerHeight(),e<0?this.activeMenu.scrollTop(i+e):s",options:{appendTo:null,autoFocus:!1,delay:300,minLength:1,position:{my:"left top",at:"left bottom",collision:"none"},source:null,change:null,close:null,focus:null,open:null,response:null,search:null,select:null},requestIndex:0,pending:0,liveRegionTimer:null,_create:function(){var i,s,n,t=this.element[0].nodeName.toLowerCase(),e="textarea"===t,t="input"===t;this.isMultiLine=e||!t&&this._isContentEditable(this.element),this.valueMethod=this.element[e||t?"val":"text"],this.isNewMenu=!0,this._addClass("ui-autocomplete-input"),this.element.attr("autocomplete","off"),this._on(this.element,{keydown:function(t){if(this.element.prop("readOnly"))s=n=i=!0;else{s=n=i=!1;var e=x.ui.keyCode;switch(t.keyCode){case e.PAGE_UP:i=!0,this._move("previousPage",t);break;case e.PAGE_DOWN:i=!0,this._move("nextPage",t);break;case e.UP:i=!0,this._keyEvent("previous",t);break;case e.DOWN:i=!0,this._keyEvent("next",t);break;case e.ENTER:this.menu.active&&(i=!0,t.preventDefault(),this.menu.select(t));break;case e.TAB:this.menu.active&&this.menu.select(t);break;case e.ESCAPE:this.menu.element.is(":visible")&&(this.isMultiLine||this._value(this.term),this.close(t),t.preventDefault());break;default:s=!0,this._searchTimeout(t)}}},keypress:function(t){if(i)return i=!1,void(this.isMultiLine&&!this.menu.element.is(":visible")||t.preventDefault());if(!s){var e=x.ui.keyCode;switch(t.keyCode){case e.PAGE_UP:this._move("previousPage",t);break;case e.PAGE_DOWN:this._move("nextPage",t);break;case e.UP:this._keyEvent("previous",t);break;case e.DOWN:this._keyEvent("next",t)}}},input:function(t){if(n)return n=!1,void t.preventDefault();this._searchTimeout(t)},focus:function(){this.selectedItem=null,this.previous=this._value()},blur:function(t){clearTimeout(this.searching),this.close(t),this._change(t)}}),this._initSource(),this.menu=x("
      ").appendTo(this._appendTo()).menu({role:null}).hide().attr({unselectable:"on"}).menu("instance"),this._addClass(this.menu.element,"ui-autocomplete","ui-front"),this._on(this.menu.element,{mousedown:function(t){t.preventDefault()},menufocus:function(t,e){var i,s;if(this.isNewMenu&&(this.isNewMenu=!1,t.originalEvent&&/^mouse/.test(t.originalEvent.type)))return this.menu.blur(),void this.document.one("mousemove",function(){x(t.target).trigger(t.originalEvent)});s=e.item.data("ui-autocomplete-item"),!1!==this._trigger("focus",t,{item:s})&&t.originalEvent&&/^key/.test(t.originalEvent.type)&&this._value(s.value),(i=e.item.attr("aria-label")||s.value)&&String.prototype.trim.call(i).length&&(clearTimeout(this.liveRegionTimer),this.liveRegionTimer=this._delay(function(){this.liveRegion.html(x("
      ").text(i))},100))},menuselect:function(t,e){var i=e.item.data("ui-autocomplete-item"),s=this.previous;this.element[0]!==x.ui.safeActiveElement(this.document[0])&&(this.element.trigger("focus"),this.previous=s,this._delay(function(){this.previous=s,this.selectedItem=i})),!1!==this._trigger("select",t,{item:i})&&this._value(i.value),this.term=this._value(),this.close(t),this.selectedItem=i}}),this.liveRegion=x("
      ",{role:"status","aria-live":"assertive","aria-relevant":"additions"}).appendTo(this.document[0].body),this._addClass(this.liveRegion,null,"ui-helper-hidden-accessible"),this._on(this.window,{beforeunload:function(){this.element.removeAttr("autocomplete")}})},_destroy:function(){clearTimeout(this.searching),this.element.removeAttr("autocomplete"),this.menu.element.remove(),this.liveRegion.remove()},_setOption:function(t,e){this._super(t,e),"source"===t&&this._initSource(),"appendTo"===t&&this.menu.element.appendTo(this._appendTo()),"disabled"===t&&e&&this.xhr&&this.xhr.abort()},_isEventTargetInWidget:function(t){var e=this.menu.element[0];return t.target===this.element[0]||t.target===e||x.contains(e,t.target)},_closeOnClickOutside:function(t){this._isEventTargetInWidget(t)||this.close()},_appendTo:function(){var t=this.options.appendTo;return t=!(t=!(t=t&&(t.jquery||t.nodeType?x(t):this.document.find(t).eq(0)))||!t[0]?this.element.closest(".ui-front, dialog"):t).length?this.document[0].body:t},_initSource:function(){var i,s,n=this;Array.isArray(this.options.source)?(i=this.options.source,this.source=function(t,e){e(x.ui.autocomplete.filter(i,t.term))}):"string"==typeof this.options.source?(s=this.options.source,this.source=function(t,e){n.xhr&&n.xhr.abort(),n.xhr=x.ajax({url:s,data:t,dataType:"json",success:function(t){e(t)},error:function(){e([])}})}):this.source=this.options.source},_searchTimeout:function(s){clearTimeout(this.searching),this.searching=this._delay(function(){var t=this.term===this._value(),e=this.menu.element.is(":visible"),i=s.altKey||s.ctrlKey||s.metaKey||s.shiftKey;t&&(e||i)||(this.selectedItem=null,this.search(null,s))},this.options.delay)},search:function(t,e){return t=null!=t?t:this._value(),this.term=this._value(),t.length").append(x("
      ").text(e.label)).appendTo(t)},_move:function(t,e){if(this.menu.element.is(":visible"))return this.menu.isFirstItem()&&/^previous/.test(t)||this.menu.isLastItem()&&/^next/.test(t)?(this.isMultiLine||this._value(this.term),void this.menu.blur()):void this.menu[t](e);this.search(null,e)},widget:function(){return this.menu.element},_value:function(){return this.valueMethod.apply(this.element,arguments)},_keyEvent:function(t,e){this.isMultiLine&&!this.menu.element.is(":visible")||(this._move(t,e),e.preventDefault())},_isContentEditable:function(t){if(!t.length)return!1;var e=t.prop("contentEditable");return"inherit"===e?this._isContentEditable(t.parent()):"true"===e}}),x.extend(x.ui.autocomplete,{escapeRegex:function(t){return t.replace(/[\-\[\]{}()*+?.,\\\^$|#\s]/g,"\\$&")},filter:function(t,e){var i=new RegExp(x.ui.autocomplete.escapeRegex(e),"i");return x.grep(t,function(t){return i.test(t.label||t.value||t)})}}),x.widget("ui.autocomplete",x.ui.autocomplete,{options:{messages:{noResults:"No search results.",results:function(t){return t+(1").text(e))},100))}});x.ui.autocomplete}); \ No newline at end of file diff --git a/target/site/testapidocs/script.js b/target/site/testapidocs/script.js new file mode 100644 index 0000000..bb9c8a2 --- /dev/null +++ b/target/site/testapidocs/script.js @@ -0,0 +1,253 @@ +/* + * Copyright (c) 2013, 2023, Oracle and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Oracle designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code 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 + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA + * or visit www.oracle.com if you need additional information or have any + * questions. + */ + +var moduleSearchIndex; +var packageSearchIndex; +var typeSearchIndex; +var memberSearchIndex; +var tagSearchIndex; + +var oddRowColor = "odd-row-color"; +var evenRowColor = "even-row-color"; +var sortAsc = "sort-asc"; +var sortDesc = "sort-desc"; +var tableTab = "table-tab"; +var activeTableTab = "active-table-tab"; + +function loadScripts(doc, tag) { + createElem(doc, tag, 'search.js'); + + createElem(doc, tag, 'module-search-index.js'); + createElem(doc, tag, 'package-search-index.js'); + createElem(doc, tag, 'type-search-index.js'); + createElem(doc, tag, 'member-search-index.js'); + createElem(doc, tag, 'tag-search-index.js'); +} + +function createElem(doc, tag, path) { + var script = doc.createElement(tag); + var scriptElement = doc.getElementsByTagName(tag)[0]; + script.src = pathtoroot + path; + scriptElement.parentNode.insertBefore(script, scriptElement); +} + +// Helper for making content containing release names comparable lexicographically +function makeComparable(s) { + return s.toLowerCase().replace(/(\d+)/g, + function(n, m) { + return ("000" + m).slice(-4); + }); +} + +// Switches between two styles depending on a condition +function toggleStyle(classList, condition, trueStyle, falseStyle) { + if (condition) { + classList.remove(falseStyle); + classList.add(trueStyle); + } else { + classList.remove(trueStyle); + classList.add(falseStyle); + } +} + +// Sorts the rows in a table lexicographically by the content of a specific column +function sortTable(header, columnIndex, columns) { + var container = header.parentElement; + var descending = header.classList.contains(sortAsc); + container.querySelectorAll("div.table-header").forEach( + function(header) { + header.classList.remove(sortAsc); + header.classList.remove(sortDesc); + } + ) + var cells = container.children; + var rows = []; + for (var i = columns; i < cells.length; i += columns) { + rows.push(Array.prototype.slice.call(cells, i, i + columns)); + } + var comparator = function(a, b) { + var ka = makeComparable(a[columnIndex].textContent); + var kb = makeComparable(b[columnIndex].textContent); + if (ka < kb) + return descending ? 1 : -1; + if (ka > kb) + return descending ? -1 : 1; + return 0; + }; + var sorted = rows.sort(comparator); + var visible = 0; + sorted.forEach(function(row) { + if (row[0].style.display !== 'none') { + var isEvenRow = visible++ % 2 === 0; + } + row.forEach(function(cell) { + toggleStyle(cell.classList, isEvenRow, evenRowColor, oddRowColor); + container.appendChild(cell); + }) + }); + toggleStyle(header.classList, descending, sortDesc, sortAsc); +} + +// Toggles the visibility of a table category in all tables in a page +function toggleGlobal(checkbox, selected, columns) { + var display = checkbox.checked ? '' : 'none'; + document.querySelectorAll("div.table-tabs").forEach(function(t) { + var id = t.parentElement.getAttribute("id"); + var selectedClass = id + "-tab" + selected; + // if selected is empty string it selects all uncategorized entries + var selectUncategorized = !Boolean(selected); + var visible = 0; + document.querySelectorAll('div.' + id) + .forEach(function(elem) { + if (selectUncategorized) { + if (elem.className.indexOf(selectedClass) === -1) { + elem.style.display = display; + } + } else if (elem.classList.contains(selectedClass)) { + elem.style.display = display; + } + if (elem.style.display === '') { + var isEvenRow = visible++ % (columns * 2) < columns; + toggleStyle(elem.classList, isEvenRow, evenRowColor, oddRowColor); + } + }); + var displaySection = visible === 0 ? 'none' : ''; + t.parentElement.style.display = displaySection; + document.querySelector("li#contents-" + id).style.display = displaySection; + }) +} + +// Shows the elements of a table belonging to a specific category +function show(tableId, selected, columns) { + if (tableId !== selected) { + document.querySelectorAll('div.' + tableId + ':not(.' + selected + ')') + .forEach(function(elem) { + elem.style.display = 'none'; + }); + } + document.querySelectorAll('div.' + selected) + .forEach(function(elem, index) { + elem.style.display = ''; + var isEvenRow = index % (columns * 2) < columns; + toggleStyle(elem.classList, isEvenRow, evenRowColor, oddRowColor); + }); + updateTabs(tableId, selected); +} + +function updateTabs(tableId, selected) { + document.getElementById(tableId + '.tabpanel') + .setAttribute('aria-labelledby', selected); + document.querySelectorAll('button[id^="' + tableId + '"]') + .forEach(function(tab, index) { + if (selected === tab.id || (tableId === selected && index === 0)) { + tab.className = activeTableTab; + tab.setAttribute('aria-selected', true); + tab.setAttribute('tabindex',0); + } else { + tab.className = tableTab; + tab.setAttribute('aria-selected', false); + tab.setAttribute('tabindex',-1); + } + }); +} + +function switchTab(e) { + var selected = document.querySelector('[aria-selected=true]'); + if (selected) { + if ((e.keyCode === 37 || e.keyCode === 38) && selected.previousSibling) { + // left or up arrow key pressed: move focus to previous tab + selected.previousSibling.click(); + selected.previousSibling.focus(); + e.preventDefault(); + } else if ((e.keyCode === 39 || e.keyCode === 40) && selected.nextSibling) { + // right or down arrow key pressed: move focus to next tab + selected.nextSibling.click(); + selected.nextSibling.focus(); + e.preventDefault(); + } + } +} + +var updateSearchResults = function() {}; + +function indexFilesLoaded() { + return moduleSearchIndex + && packageSearchIndex + && typeSearchIndex + && memberSearchIndex + && tagSearchIndex; +} +// Copy the contents of the local snippet to the clipboard +function copySnippet(button) { + copyToClipboard(button.nextElementSibling.innerText); + switchCopyLabel(button, button.firstElementChild); +} +function copyToClipboard(content) { + var textarea = document.createElement("textarea"); + textarea.style.height = 0; + document.body.appendChild(textarea); + textarea.value = content; + textarea.select(); + document.execCommand("copy"); + document.body.removeChild(textarea); +} +function switchCopyLabel(button, span) { + var copied = span.getAttribute("data-copied"); + button.classList.add("visible"); + var initialLabel = span.innerHTML; + span.innerHTML = copied; + setTimeout(function() { + button.classList.remove("visible"); + setTimeout(function() { + if (initialLabel !== copied) { + span.innerHTML = initialLabel; + } + }, 100); + }, 1900); +} +// Workaround for scroll position not being included in browser history (8249133) +document.addEventListener("DOMContentLoaded", function(e) { + var contentDiv = document.querySelector("div.flex-content"); + window.addEventListener("popstate", function(e) { + if (e.state !== null) { + contentDiv.scrollTop = e.state; + } + }); + window.addEventListener("hashchange", function(e) { + history.replaceState(contentDiv.scrollTop, document.title); + }); + var timeoutId; + contentDiv.addEventListener("scroll", function(e) { + if (timeoutId) { + clearTimeout(timeoutId); + } + timeoutId = setTimeout(function() { + history.replaceState(contentDiv.scrollTop, document.title); + }, 100); + }); + if (!location.hash) { + history.replaceState(contentDiv.scrollTop, document.title); + } +}); diff --git a/target/site/testapidocs/search-page.js b/target/site/testapidocs/search-page.js new file mode 100644 index 0000000..540c90f --- /dev/null +++ b/target/site/testapidocs/search-page.js @@ -0,0 +1,284 @@ +/* + * Copyright (c) 2022, 2023, Oracle and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Oracle designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code 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 + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA + * or visit www.oracle.com if you need additional information or have any + * questions. + */ + +"use strict"; +$(function() { + var copy = $("#page-search-copy"); + var expand = $("#page-search-expand"); + var searchLink = $("span#page-search-link"); + var redirect = $("input#search-redirect"); + function setSearchUrlTemplate() { + var href = document.location.href.split(/[#?]/)[0]; + href += "?q=" + "%s"; + if (redirect.is(":checked")) { + href += "&r=1"; + } + searchLink.html(href); + copy[0].onmouseenter(); + } + function copyLink(e) { + copyToClipboard(this.previousSibling.innerText); + switchCopyLabel(this, this.lastElementChild); + } + copy.click(copyLink); + copy[0].onmouseenter = function() {}; + redirect.click(setSearchUrlTemplate); + setSearchUrlTemplate(); + copy.prop("disabled", false); + redirect.prop("disabled", false); + expand.click(function (e) { + var searchInfo = $("div.page-search-info"); + if(this.parentElement.hasAttribute("open")) { + searchInfo.attr("style", "border-width: 0;"); + } else { + searchInfo.attr("style", "border-width: 1px;").height(searchInfo.prop("scrollHeight")); + } + }); +}); +$(window).on("load", function() { + var input = $("#page-search-input"); + var reset = $("#page-search-reset"); + var notify = $("#page-search-notify"); + var resultSection = $("div#result-section"); + var resultContainer = $("div#result-container"); + var searchTerm = ""; + var activeTab = ""; + var fixedTab = false; + var visibleTabs = []; + var feelingLucky = false; + function renderResults(result) { + if (!result.length) { + notify.html(messages.noResult); + } else if (result.length === 1) { + notify.html(messages.oneResult); + } else { + notify.html(messages.manyResults.replace("{0}", result.length)); + } + resultContainer.empty(); + var r = { + "types": [], + "members": [], + "packages": [], + "modules": [], + "searchTags": [] + }; + for (var i in result) { + var item = result[i]; + var arr = r[item.category]; + arr.push(item); + } + if (!activeTab || r[activeTab].length === 0 || !fixedTab) { + Object.keys(r).reduce(function(prev, curr) { + if (r[curr].length > 0 && r[curr][0].score > prev) { + activeTab = curr; + return r[curr][0].score; + } + return prev; + }, 0); + } + if (feelingLucky && activeTab) { + notify.html(messages.redirecting) + var firstItem = r[activeTab][0]; + window.location = getURL(firstItem.indexItem, firstItem.category); + return; + } + if (result.length > 20) { + if (searchTerm[searchTerm.length - 1] === ".") { + if (activeTab === "types" && r["members"].length > r["types"].length) { + activeTab = "members"; + } else if (activeTab === "packages" && r["types"].length > r["packages"].length) { + activeTab = "types"; + } + } + } + var categoryCount = Object.keys(r).reduce(function(prev, curr) { + return prev + (r[curr].length > 0 ? 1 : 0); + }, 0); + visibleTabs = []; + var tabContainer = $("
      ").appendTo(resultContainer); + for (var key in r) { + var id = "#result-tab-" + key.replace("searchTags", "search_tags"); + if (r[key].length) { + var count = r[key].length >= 1000 ? "999+" : r[key].length; + if (result.length > 20 && categoryCount > 1) { + var button = $("").appendTo(tabContainer); + button.click(key, function(e) { + fixedTab = true; + renderResult(e.data, $(this)); + }); + visibleTabs.push(key); + } else { + $("" + categories[key] + + " (" + count + ")").appendTo(tabContainer); + renderTable(key, r[key]).appendTo(resultContainer); + tabContainer = $("
      ").appendTo(resultContainer); + + } + } + } + if (activeTab && result.length > 20 && categoryCount > 1) { + $("button#result-tab-" + activeTab).addClass("active-table-tab"); + renderTable(activeTab, r[activeTab]).appendTo(resultContainer); + } + resultSection.show(); + function renderResult(category, button) { + activeTab = category; + setSearchUrl(); + resultContainer.find("div.summary-table").remove(); + renderTable(activeTab, r[activeTab]).appendTo(resultContainer); + button.siblings().removeClass("active-table-tab"); + button.addClass("active-table-tab"); + } + } + function selectTab(category) { + $("button#result-tab-" + category).click(); + } + function renderTable(category, items) { + var table = $("
      ") + .addClass(category === "modules" + ? "one-column-search-results" + : "two-column-search-results"); + var col1, col2; + if (category === "modules") { + col1 = "Module"; + } else if (category === "packages") { + col1 = "Module"; + col2 = "Package"; + } else if (category === "types") { + col1 = "Package"; + col2 = "Class" + } else if (category === "members") { + col1 = "Class"; + col2 = "Member"; + } else if (category === "searchTags") { + col1 = "Location"; + col2 = "Name"; + } + $("
      " + col1 + "
      ").appendTo(table); + if (category !== "modules") { + $("
      " + col2 + "
      ").appendTo(table); + } + $.each(items, function(index, item) { + var rowColor = index % 2 ? "odd-row-color" : "even-row-color"; + renderItem(item, table, rowColor); + }); + return table; + } + function renderItem(item, table, rowColor) { + var label = getHighlightedText(item.input, item.boundaries, item.prefix.length, item.input.length); + var link = $("") + .attr("href", getURL(item.indexItem, item.category)) + .attr("tabindex", "0") + .addClass("search-result-link") + .html(label); + var container = getHighlightedText(item.input, item.boundaries, 0, item.prefix.length - 1); + if (item.category === "searchTags") { + container = item.indexItem.h || ""; + } + if (item.category !== "modules") { + $("
      ").html(container).addClass("col-plain").addClass(rowColor).appendTo(table); + } + $("
      ").html(link).addClass("col-last").addClass(rowColor).appendTo(table); + } + var timeout; + function schedulePageSearch() { + if (timeout) { + clearTimeout(timeout); + } + timeout = setTimeout(function () { + doPageSearch() + }, 100); + } + function doPageSearch() { + setSearchUrl(); + var term = searchTerm = input.val().trim(); + if (term === "") { + notify.html(messages.enterTerm); + activeTab = ""; + fixedTab = false; + resultContainer.empty(); + resultSection.hide(); + } else { + notify.html(messages.searching); + doSearch({ term: term, maxResults: 1200 }, renderResults); + } + } + function setSearchUrl() { + var query = input.val().trim(); + var url = document.location.pathname; + if (query) { + url += "?q=" + encodeURI(query); + if (activeTab && fixedTab) { + url += "&c=" + activeTab; + } + } + history.replaceState({query: query}, "", url); + } + input.on("input", function(e) { + feelingLucky = false; + schedulePageSearch(); + }); + $(document).keydown(function(e) { + if ((e.ctrlKey || e.metaKey) && (e.key === "ArrowLeft" || e.key === "ArrowRight")) { + if (activeTab && visibleTabs.length > 1) { + var idx = visibleTabs.indexOf(activeTab); + idx += e.key === "ArrowLeft" ? visibleTabs.length - 1 : 1; + selectTab(visibleTabs[idx % visibleTabs.length]); + return false; + } + } + }); + reset.click(function() { + notify.html(messages.enterTerm); + resultSection.hide(); + activeTab = ""; + fixedTab = false; + resultContainer.empty(); + input.val('').focus(); + setSearchUrl(); + }); + input.prop("disabled", false); + reset.prop("disabled", false); + + var urlParams = new URLSearchParams(window.location.search); + if (urlParams.has("q")) { + input.val(urlParams.get("q")) + } + if (urlParams.has("c")) { + activeTab = urlParams.get("c"); + fixedTab = true; + } + if (urlParams.get("r")) { + feelingLucky = true; + } + if (input.val()) { + doPageSearch(); + } else { + notify.html(messages.enterTerm); + } + input.select().focus(); +}); diff --git a/target/site/testapidocs/search.html b/target/site/testapidocs/search.html new file mode 100644 index 0000000..40ffe3c --- /dev/null +++ b/target/site/testapidocs/search.html @@ -0,0 +1,77 @@ + + + + +Search (Vision Skills Progression Tracker 1.0.0-beta Test API) + + + + + + + + + + + + + + +
      + +
      +
      +

      Search

      +
      + + +
      +Additional resources +
      +
      +
      +

      The help page provides an introduction to the scope and syntax of JavaDoc search.

      +

      You can use the <ctrl> or <cmd> keys in combination with the left and right arrow keys to switch between result tabs in this page.

      +

      The URL template below may be used to configure this page as a search engine in browsers that support this feature. It has been tested to work in Google Chrome and Mozilla Firefox. Note that other browsers may not support this feature or require a different URL format.

      +link +

      + +

      +
      +

      Loading search index...

      + +
      + +
      +
      + + diff --git a/target/site/testapidocs/search.js b/target/site/testapidocs/search.js new file mode 100644 index 0000000..d398670 --- /dev/null +++ b/target/site/testapidocs/search.js @@ -0,0 +1,458 @@ +/* + * Copyright (c) 2015, 2023, Oracle and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Oracle designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code 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 + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA + * or visit www.oracle.com if you need additional information or have any + * questions. + */ +"use strict"; +const messages = { + enterTerm: "Enter a search term", + noResult: "No results found", + oneResult: "Found one result", + manyResults: "Found {0} results", + loading: "Loading search index...", + searching: "Searching...", + redirecting: "Redirecting to first result...", + linkIcon: "Link icon", + linkToSection: "Link to this section" +} +const categories = { + modules: "Modules", + packages: "Packages", + types: "Classes and Interfaces", + members: "Members", + searchTags: "Search Tags" +}; +const highlight = "$&"; +const NO_MATCH = {}; +const MAX_RESULTS = 300; +function checkUnnamed(name, separator) { + return name === "" || !name ? "" : name + separator; +} +function escapeHtml(str) { + return str.replace(//g, ">"); +} +function getHighlightedText(str, boundaries, from, to) { + var start = from; + var text = ""; + for (var i = 0; i < boundaries.length; i += 2) { + var b0 = boundaries[i]; + var b1 = boundaries[i + 1]; + if (b0 >= to || b1 <= from) { + continue; + } + text += escapeHtml(str.slice(start, Math.max(start, b0))); + text += ""; + text += escapeHtml(str.slice(Math.max(start, b0), Math.min(to, b1))); + text += ""; + start = Math.min(to, b1); + } + text += escapeHtml(str.slice(start, to)); + return text; +} +function getURLPrefix(item, category) { + var urlPrefix = ""; + var slash = "/"; + if (category === "modules") { + return item.l + slash; + } else if (category === "packages" && item.m) { + return item.m + slash; + } else if (category === "types" || category === "members") { + if (item.m) { + urlPrefix = item.m + slash; + } else { + $.each(packageSearchIndex, function(index, it) { + if (it.m && item.p === it.l) { + urlPrefix = it.m + slash; + } + }); + } + } + return urlPrefix; +} +function getURL(item, category) { + if (item.url) { + return item.url; + } + var url = getURLPrefix(item, category); + if (category === "modules") { + url += "module-summary.html"; + } else if (category === "packages") { + if (item.u) { + url = item.u; + } else { + url += item.l.replace(/\./g, '/') + "/package-summary.html"; + } + } else if (category === "types") { + if (item.u) { + url = item.u; + } else { + url += checkUnnamed(item.p, "/").replace(/\./g, '/') + item.l + ".html"; + } + } else if (category === "members") { + url += checkUnnamed(item.p, "/").replace(/\./g, '/') + item.c + ".html" + "#"; + if (item.u) { + url += item.u; + } else { + url += item.l; + } + } else if (category === "searchTags") { + url += item.u; + } + item.url = url; + return url; +} +function createMatcher(term, camelCase) { + if (camelCase && !isUpperCase(term)) { + return null; // no need for camel-case matcher for lower case query + } + var pattern = ""; + var upperCase = []; + term.trim().split(/\s+/).forEach(function(w, index, array) { + var tokens = w.split(/(?=[A-Z,.()<>?[\/])/); + for (var i = 0; i < tokens.length; i++) { + var s = tokens[i]; + // ',' and '?' are the only delimiters commonly followed by space in java signatures + pattern += "(" + $.ui.autocomplete.escapeRegex(s).replace(/[,?]/g, "$&\\s*?") + ")"; + upperCase.push(false); + var isWordToken = /\w$/.test(s); + if (isWordToken) { + if (i === tokens.length - 1 && index < array.length - 1) { + // space in query string matches all delimiters + pattern += "(.*?)"; + upperCase.push(isUpperCase(s[0])); + } else { + if (!camelCase && isUpperCase(s) && s.length === 1) { + pattern += "()"; + } else { + pattern += "([a-z0-9$<>?[\\]]*?)"; + } + upperCase.push(isUpperCase(s[0])); + } + } else { + pattern += "()"; + upperCase.push(false); + } + } + }); + var re = new RegExp(pattern, "gi"); + re.upperCase = upperCase; + return re; +} +function findMatch(matcher, input, startOfName, endOfName) { + var from = startOfName; + matcher.lastIndex = from; + var match = matcher.exec(input); + // Expand search area until we get a valid result or reach the beginning of the string + while (!match || match.index + match[0].length < startOfName || endOfName < match.index) { + if (from === 0) { + return NO_MATCH; + } + from = input.lastIndexOf(".", from - 2) + 1; + matcher.lastIndex = from; + match = matcher.exec(input); + } + var boundaries = []; + var matchEnd = match.index + match[0].length; + var score = 5; + var start = match.index; + var prevEnd = -1; + for (var i = 1; i < match.length; i += 2) { + var isUpper = isUpperCase(input[start]); + var isMatcherUpper = matcher.upperCase[i]; + // capturing groups come in pairs, match and non-match + boundaries.push(start, start + match[i].length); + // make sure groups are anchored on a left word boundary + var prevChar = input[start - 1] || ""; + var nextChar = input[start + 1] || ""; + if (start !== 0 && !/[\W_]/.test(prevChar) && !/[\W_]/.test(input[start])) { + if (isUpper && (isLowerCase(prevChar) || isLowerCase(nextChar))) { + score -= 0.1; + } else if (isMatcherUpper && start === prevEnd) { + score -= isUpper ? 0.1 : 1.0; + } else { + return NO_MATCH; + } + } + prevEnd = start + match[i].length; + start += match[i].length + match[i + 1].length; + + // lower score for parts of the name that are missing + if (match[i + 1] && prevEnd < endOfName) { + score -= rateNoise(match[i + 1]); + } + } + // lower score if a type name contains unmatched camel-case parts + if (input[matchEnd - 1] !== "." && endOfName > matchEnd) + score -= rateNoise(input.slice(matchEnd, endOfName)); + score -= rateNoise(input.slice(0, Math.max(startOfName, match.index))); + + if (score <= 0) { + return NO_MATCH; + } + return { + input: input, + score: score, + boundaries: boundaries + }; +} +function isUpperCase(s) { + return s !== s.toLowerCase(); +} +function isLowerCase(s) { + return s !== s.toUpperCase(); +} +function rateNoise(str) { + return (str.match(/([.(])/g) || []).length / 5 + + (str.match(/([A-Z]+)/g) || []).length / 10 + + str.length / 20; +} +function doSearch(request, response) { + var term = request.term.trim(); + var maxResults = request.maxResults || MAX_RESULTS; + if (term.length === 0) { + return this.close(); + } + var matcher = { + plainMatcher: createMatcher(term, false), + camelCaseMatcher: createMatcher(term, true) + } + var indexLoaded = indexFilesLoaded(); + + function getPrefix(item, category) { + switch (category) { + case "packages": + return checkUnnamed(item.m, "/"); + case "types": + return checkUnnamed(item.p, "."); + case "members": + return checkUnnamed(item.p, ".") + item.c + "."; + default: + return ""; + } + } + function useQualifiedName(category) { + switch (category) { + case "packages": + return /[\s/]/.test(term); + case "types": + case "members": + return /[\s.]/.test(term); + default: + return false; + } + } + function searchIndex(indexArray, category) { + var matches = []; + if (!indexArray) { + if (!indexLoaded) { + matches.push({ l: messages.loading, category: category }); + } + return matches; + } + $.each(indexArray, function (i, item) { + var prefix = getPrefix(item, category); + var simpleName = item.l; + var qualifiedName = prefix + simpleName; + var useQualified = useQualifiedName(category); + var input = useQualified ? qualifiedName : simpleName; + var startOfName = useQualified ? prefix.length : 0; + var endOfName = category === "members" && input.indexOf("(", startOfName) > -1 + ? input.indexOf("(", startOfName) : input.length; + var m = findMatch(matcher.plainMatcher, input, startOfName, endOfName); + if (m === NO_MATCH && matcher.camelCaseMatcher) { + m = findMatch(matcher.camelCaseMatcher, input, startOfName, endOfName); + } + if (m !== NO_MATCH) { + m.indexItem = item; + m.prefix = prefix; + m.category = category; + if (!useQualified) { + m.input = qualifiedName; + m.boundaries = m.boundaries.map(function(b) { + return b + prefix.length; + }); + } + matches.push(m); + } + return true; + }); + return matches.sort(function(e1, e2) { + return e2.score - e1.score; + }).slice(0, maxResults); + } + + var result = searchIndex(moduleSearchIndex, "modules") + .concat(searchIndex(packageSearchIndex, "packages")) + .concat(searchIndex(typeSearchIndex, "types")) + .concat(searchIndex(memberSearchIndex, "members")) + .concat(searchIndex(tagSearchIndex, "searchTags")); + + if (!indexLoaded) { + updateSearchResults = function() { + doSearch(request, response); + } + } else { + updateSearchResults = function() {}; + } + response(result); +} +// JQuery search menu implementation +$.widget("custom.catcomplete", $.ui.autocomplete, { + _create: function() { + this._super(); + this.widget().menu("option", "items", "> .result-item"); + // workaround for search result scrolling + this.menu._scrollIntoView = function _scrollIntoView( item ) { + var borderTop, paddingTop, offset, scroll, elementHeight, itemHeight; + if ( this._hasScroll() ) { + borderTop = parseFloat( $.css( this.activeMenu[ 0 ], "borderTopWidth" ) ) || 0; + paddingTop = parseFloat( $.css( this.activeMenu[ 0 ], "paddingTop" ) ) || 0; + offset = item.offset().top - this.activeMenu.offset().top - borderTop - paddingTop; + scroll = this.activeMenu.scrollTop(); + elementHeight = this.activeMenu.height() - 26; + itemHeight = item.outerHeight(); + + if ( offset < 0 ) { + this.activeMenu.scrollTop( scroll + offset ); + } else if ( offset + itemHeight > elementHeight ) { + this.activeMenu.scrollTop( scroll + offset - elementHeight + itemHeight ); + } + } + }; + }, + _renderMenu: function(ul, items) { + var currentCategory = ""; + var widget = this; + widget.menu.bindings = $(); + $.each(items, function(index, item) { + if (item.category && item.category !== currentCategory) { + ul.append("
    • " + categories[item.category] + "
    • "); + currentCategory = item.category; + } + var li = widget._renderItemData(ul, item); + if (item.category) { + li.attr("aria-label", categories[item.category] + " : " + item.l); + } else { + li.attr("aria-label", item.l); + } + li.attr("class", "result-item"); + }); + ul.append(""); + }, + _renderItem: function(ul, item) { + var li = $("
    • ").appendTo(ul); + var div = $("
      ").appendTo(li); + var label = item.l + ? item.l + : getHighlightedText(item.input, item.boundaries, 0, item.input.length); + var idx = item.indexItem; + if (item.category === "searchTags" && idx && idx.h) { + if (idx.d) { + div.html(label + " (" + idx.h + ")
      " + + idx.d + "
      "); + } else { + div.html(label + " (" + idx.h + ")"); + } + } else { + div.html(label); + } + return li; + } +}); +$(function() { + var expanded = false; + var windowWidth; + function collapse() { + if (expanded) { + $("div#navbar-top").removeAttr("style"); + $("button#navbar-toggle-button") + .removeClass("expanded") + .attr("aria-expanded", "false"); + expanded = false; + } + } + $("button#navbar-toggle-button").click(function (e) { + if (expanded) { + collapse(); + } else { + var navbar = $("div#navbar-top"); + navbar.height(navbar.prop("scrollHeight")); + $("button#navbar-toggle-button") + .addClass("expanded") + .attr("aria-expanded", "true"); + expanded = true; + windowWidth = window.innerWidth; + } + }); + $("ul.sub-nav-list-small li a").click(collapse); + $("input#search-input").focus(collapse); + $("main").click(collapse); + $("section[id] > :header, :header[id], :header:has(a[id])").each(function(idx, el) { + // Create anchor links for headers with an associated id attribute + var hdr = $(el); + var id = hdr.attr("id") || hdr.parent("section").attr("id") || hdr.children("a").attr("id"); + if (id) { + hdr.append(" " + messages.linkIcon +""); + } + }); + $(window).on("orientationchange", collapse).on("resize", function(e) { + if (expanded && windowWidth !== window.innerWidth) collapse(); + }); + var search = $("#search-input"); + var reset = $("#reset-button"); + search.catcomplete({ + minLength: 1, + delay: 200, + source: doSearch, + response: function(event, ui) { + if (!ui.content.length) { + ui.content.push({ l: messages.noResult }); + } else { + $("#search-input").empty(); + } + }, + autoFocus: true, + focus: function(event, ui) { + return false; + }, + position: { + collision: "flip" + }, + select: function(event, ui) { + if (ui.item.indexItem) { + var url = getURL(ui.item.indexItem, ui.item.category); + window.location.href = pathtoroot + url; + $("#search-input").focus(); + } + } + }); + search.val(''); + search.prop("disabled", false); + reset.prop("disabled", false); + reset.click(function() { + search.val('').focus(); + }); + search.focus(); +}); diff --git a/target/site/testapidocs/src-html/com/studentgui/apphelpers/DatabaseContactLogTest.html b/target/site/testapidocs/src-html/com/studentgui/apphelpers/DatabaseContactLogTest.html new file mode 100644 index 0000000..68c99c6 --- /dev/null +++ b/target/site/testapidocs/src-html/com/studentgui/apphelpers/DatabaseContactLogTest.html @@ -0,0 +1,103 @@ + + + + +Source code + + + + + + +
      +
      +
      001package com.studentgui.apphelpers;
      +002
      +003import java.time.LocalDate;
      +004
      +005import static org.junit.jupiter.api.Assertions.assertEquals;
      +006import static org.junit.jupiter.api.Assertions.assertNotNull;
      +007import org.junit.jupiter.api.Test;
      +008
      +009public class DatabaseContactLogTest {
      +010
      +011    @Test
      +012    public void testSaveAndFetchContactLog() throws Exception {
      +013        SqlGenerate.initializeDatabase();
      +014        String student = "Test Student";
      +015        int sid = Database.getOrCreateStudent(student);
      +016        int pt = Database.getOrCreateProgressType("ContactLog");
      +017        int sessionId = Database.createProgressSession(sid, pt, LocalDate.now());
      +018        Database.saveContactLog(sessionId, student, LocalDate.now().toString(), "Guardian A", "Phone", "+1234567890", "a@example.com", "Left voicemail", "General summary", "Specific item", "Detailed notes");
      +019    com.studentgui.apphelpers.dto.ContactPayload fetched = Database.fetchLatestContactLog(student);
      +020    assertNotNull(fetched);
      +021    assertEquals("Guardian A", fetched.guardian);
      +022    assertEquals("+1234567890", fetched.phone);
      +023    assertEquals("Detailed notes", fetched.notes);
      +024    }
      +025}
      +
      +
      +
      +
      +
      +
      +
      +
      +
      +
      +
      +
      +
      +
      +
      +
      +
      +
      +
      +
      +
      +
      +
      +
      +
      +
      +
      +
      +
      +
      +
      +
      +
      +
      +
      +
      +
      +
      +
      +
      +
      +
      +
      +
      +
      +
      +
      +
      +
      +
      +
      +
      +
      +
      +
      +
      +
      +
      +
      +
      +
      +
      +
      + + diff --git a/target/site/testapidocs/src-html/com/studentgui/apphelpers/SessionJsonWriterTest.html b/target/site/testapidocs/src-html/com/studentgui/apphelpers/SessionJsonWriterTest.html new file mode 100644 index 0000000..7358645 --- /dev/null +++ b/target/site/testapidocs/src-html/com/studentgui/apphelpers/SessionJsonWriterTest.html @@ -0,0 +1,127 @@ + + + + +Source code + + + + + + +
      +
      +
      001package com.studentgui.apphelpers;
      +002
      +003import java.nio.file.Files;
      +004import java.nio.file.Path;
      +005
      +006import static org.junit.jupiter.api.Assertions.assertEquals;
      +007import static org.junit.jupiter.api.Assertions.assertNotNull;
      +008import static org.junit.jupiter.api.Assertions.assertTrue;
      +009import org.junit.jupiter.api.Test;
      +010
      +011import com.fasterxml.jackson.databind.JsonNode;
      +012import com.fasterxml.jackson.databind.ObjectMapper;
      +013import com.studentgui.apphelpers.dto.NotesPayload;
      +014
      +015/**
      +016 * Unit test for SessionJsonWriter to verify envelope and filename format.
      +017 */
      +018public class SessionJsonWriterTest {
      +019
      +020    @Test
      +021    public void writeSessionJson_includesSessionIdAndPayload() throws Exception {
      +022        String student = "UnitTestStudent-" + System.nanoTime();
      +023        int sessionId = 314159;
      +024        NotesPayload payload = new NotesPayload(sessionId, "unit test notes payload");
      +025
      +026        Path out = SessionJsonWriter.writeSessionJson(student, "UnitTestPage", payload, sessionId);
      +027        assertNotNull(out, "writeSessionJson should return a path");
      +028        assertTrue(Files.exists(out), "written file should exist");
      +029
      +030        String fname = out.getFileName().toString();
      +031        assertTrue(fname.contains("UnitTestPage"), "filename should contain page name");
      +032        assertTrue(fname.contains("-session-" + sessionId), "filename should include session id segment");
      +033
      +034        byte[] data = Files.readAllBytes(out);
      +035        ObjectMapper m = new ObjectMapper();
      +036        JsonNode root = m.readTree(data);
      +037        assertEquals(student, root.get("student").asText());
      +038        assertEquals("UnitTestPage", root.get("page").asText());
      +039        assertTrue(root.has("sessionId"));
      +040        assertEquals(sessionId, root.get("sessionId").asInt());
      +041
      +042        JsonNode payloadNode = root.get("payload");
      +043        assertNotNull(payloadNode);
      +044        assertEquals("unit test notes payload", payloadNode.get("notes").asText());
      +045
      +046        // cleanup - Files.deleteIfExists throws IOException; catch that specifically
      +047        try { Files.deleteIfExists(out); } catch (java.io.IOException ex) { /* best-effort cleanup */ }
      +048    }
      +049}
      +
      +
      +
      +
      +
      +
      +
      +
      +
      +
      +
      +
      +
      +
      +
      +
      +
      +
      +
      +
      +
      +
      +
      +
      +
      +
      +
      +
      +
      +
      +
      +
      +
      +
      +
      +
      +
      +
      +
      +
      +
      +
      +
      +
      +
      +
      +
      +
      +
      +
      +
      +
      +
      +
      +
      +
      +
      +
      +
      +
      +
      +
      +
      + + diff --git a/target/site/testapidocs/src-html/com/studentgui/apphelpers/SqlGenerateTest.html b/target/site/testapidocs/src-html/com/studentgui/apphelpers/SqlGenerateTest.html new file mode 100644 index 0000000..0126d65 --- /dev/null +++ b/target/site/testapidocs/src-html/com/studentgui/apphelpers/SqlGenerateTest.html @@ -0,0 +1,106 @@ + + + + +Source code + + + + + + +
      +
      +
      001package com.studentgui.apphelpers;
      +002
      +003import java.nio.file.Files;
      +004import java.nio.file.Path;
      +005import java.sql.Connection;
      +006import java.sql.DriverManager;
      +007import java.sql.ResultSet;
      +008import java.sql.Statement;
      +009
      +010import static org.junit.jupiter.api.Assertions.assertTrue;
      +011import org.junit.jupiter.api.Test;
      +012
      +013public class SqlGenerateTest {
      +014
      +015    @Test
      +016    public void testInitializeCreatesContactLogTable() throws Exception {
      +017        SqlGenerate.initializeDatabase();
      +018        Path db = Helpers.DATABASE_PATH;
      +019        assertTrue(Files.exists(db));
      +020        String url = "jdbc:sqlite:" + db.toString();
      +021        try (Connection c = DriverManager.getConnection(url)) {
      +022            try (Statement st = c.createStatement()) {
      +023                ResultSet rs = st.executeQuery("SELECT name FROM sqlite_master WHERE type='table' AND name='ContactLog'");
      +024                assertTrue(rs.next(), "ContactLog table should exist after initialization");
      +025            }
      +026        }
      +027    }
      +028}
      +
      +
      +
      +
      +
      +
      +
      +
      +
      +
      +
      +
      +
      +
      +
      +
      +
      +
      +
      +
      +
      +
      +
      +
      +
      +
      +
      +
      +
      +
      +
      +
      +
      +
      +
      +
      +
      +
      +
      +
      +
      +
      +
      +
      +
      +
      +
      +
      +
      +
      +
      +
      +
      +
      +
      +
      +
      +
      +
      +
      +
      +
      +
      + + diff --git a/target/site/testapidocs/src-html/com/studentgui/apppages/JLineGraphDeterministicJitterTest.html b/target/site/testapidocs/src-html/com/studentgui/apppages/JLineGraphDeterministicJitterTest.html new file mode 100644 index 0000000..bfa9520 --- /dev/null +++ b/target/site/testapidocs/src-html/com/studentgui/apppages/JLineGraphDeterministicJitterTest.html @@ -0,0 +1,120 @@ + + + + +Source code + + + + + + +
      +
      +
      001package com.studentgui.apppages;
      +002
      +003import java.lang.reflect.Method;
      +004
      +005import static org.junit.jupiter.api.Assertions.assertArrayEquals;
      +006import org.junit.jupiter.api.Test;
      +007
      +008/**
      +009 * Small unit test to validate deterministic jitter reproducibility.
      +010 * The test uses reflection to invoke the private addJitter(double) helper
      +011 * on two separate JLineGraph instances configured with the same seed and
      +012 * deterministic mode. The produced sequences must match exactly.
      +013 */
      +014public class JLineGraphDeterministicJitterTest {
      +015
      +016    @Test
      +017    public void deterministicJitterProducesSameSequence() throws Exception {
      +018        JLineGraph g1 = new JLineGraph();
      +019        JLineGraph g2 = new JLineGraph();
      +020
      +021        g1.setJitterDeterministic(true);
      +022        g2.setJitterDeterministic(true);
      +023        g1.setJitterSeed(123456789L);
      +024        g2.setJitterSeed(123456789L);
      +025
      +026        Method addJitter = JLineGraph.class.getDeclaredMethod("addJitter", double.class);
      +027        addJitter.setAccessible(true);
      +028
      +029        final int N = 10;
      +030        double[] seq1 = new double[N];
      +031        double[] seq2 = new double[N];
      +032
      +033        double base = 2.0;
      +034        for (int i = 0; i < N; i++) {
      +035            seq1[i] = (double) addJitter.invoke(g1, base);
      +036            seq2[i] = (double) addJitter.invoke(g2, base);
      +037        }
      +038
      +039        // sequences must match exactly when using same seed
      +040        assertArrayEquals(seq1, seq2, 0.0);
      +041    }
      +042}
      +
      +
      +
      +
      +
      +
      +
      +
      +
      +
      +
      +
      +
      +
      +
      +
      +
      +
      +
      +
      +
      +
      +
      +
      +
      +
      +
      +
      +
      +
      +
      +
      +
      +
      +
      +
      +
      +
      +
      +
      +
      +
      +
      +
      +
      +
      +
      +
      +
      +
      +
      +
      +
      +
      +
      +
      +
      +
      +
      +
      +
      +
      +
      + + diff --git a/target/site/testapidocs/src-html/com/studentgui/test/BrailleDatabaseTest.html b/target/site/testapidocs/src-html/com/studentgui/test/BrailleDatabaseTest.html new file mode 100644 index 0000000..8cbcfae --- /dev/null +++ b/target/site/testapidocs/src-html/com/studentgui/test/BrailleDatabaseTest.html @@ -0,0 +1,126 @@ + + + + +Source code + + + + + + +
      +
      +
      001package com.studentgui.test;
      +002
      +003import java.time.LocalDate;
      +004import java.util.List;
      +005
      +006import static org.junit.jupiter.api.Assertions.assertNotNull;
      +007import static org.junit.jupiter.api.Assertions.assertTrue;
      +008import org.junit.jupiter.api.Test;
      +009
      +010import com.studentgui.apphelpers.Helpers;
      +011import com.studentgui.apphelpers.SqlGenerate;
      +012
      +013/**
      +014 * Small integration-style unit test that uses the normalized Database helper methods
      +015 * to create a student, a progress type, ensure parts, insert one session and fetch the
      +016 * latest results. This runs headless and doesn't start any UI components.
      +017 */
      +018public class BrailleDatabaseTest {
      +019
      +020    @Test
      +021    public void smokeDatabaseFlow() throws Exception {
      +022        // Ensure app folders and DB exist
      +023        Helpers.createFolderHierarchy();
      +024        SqlGenerate.initializeDatabase();
      +025
      +026        int studentId = com.studentgui.apphelpers.Database.getOrCreateStudent("JUnit Smoke Student");
      +027        assertTrue(studentId > 0);
      +028
      +029        int ptId = com.studentgui.apphelpers.Database.getOrCreateProgressType("Braille");
      +030        assertTrue(ptId > 0);
      +031
      +032        String[] codes = new String[5];
      +033        int[] scores = new int[5];
      +034        for (int i = 0; i < 5; i++) { codes[i] = "P" + (i+1); scores[i] = (i % 3) + 1; }
      +035        com.studentgui.apphelpers.Database.ensureAssessmentParts(ptId, codes);
      +036
      +037        int sessionId = com.studentgui.apphelpers.Database.createProgressSession(studentId, ptId, LocalDate.now());
      +038        assertTrue(sessionId > 0);
      +039
      +040        com.studentgui.apphelpers.Database.insertAssessmentResults(sessionId, ptId, codes, scores);
      +041
      +042        List<List<Integer>> rows = com.studentgui.apphelpers.Database.fetchLatestAssessmentResults("JUnit Smoke Student", "Braille", 5);
      +043        assertNotNull(rows);
      +044
      +045        // At least one row should be returned
      +046        assertTrue(rows.size() >= 1);
      +047    }
      +048}
      +
      +
      +
      +
      +
      +
      +
      +
      +
      +
      +
      +
      +
      +
      +
      +
      +
      +
      +
      +
      +
      +
      +
      +
      +
      +
      +
      +
      +
      +
      +
      +
      +
      +
      +
      +
      +
      +
      +
      +
      +
      +
      +
      +
      +
      +
      +
      +
      +
      +
      +
      +
      +
      +
      +
      +
      +
      +
      +
      +
      +
      +
      +
      + + diff --git a/target/site/testapidocs/src-html/com/studentgui/test/BrailleSmokeTest.html b/target/site/testapidocs/src-html/com/studentgui/test/BrailleSmokeTest.html new file mode 100644 index 0000000..4967e95 --- /dev/null +++ b/target/site/testapidocs/src-html/com/studentgui/test/BrailleSmokeTest.html @@ -0,0 +1,120 @@ + + + + +Source code + + + + + + +
      +
      +
      001package com.studentgui.test;
      +002
      +003import static org.junit.jupiter.api.Assertions.assertNotNull;
      +004
      +005import java.time.LocalDate;
      +006import java.util.List;
      +007
      +008import org.junit.jupiter.api.Test;
      +009
      +010import com.studentgui.apphelpers.Helpers;
      +011import com.studentgui.apphelpers.SqlGenerate;
      +012import com.studentgui.apppages.JLineGraph;
      +013
      +014/**
      +015 * JUnit replacement for the legacy Braille smoke main. Exercises the
      +016 * normalized database APIs and invokes JLineGraph.updateWithData(...) to
      +017 * verify plumbing without launching the full GUI.
      +018 */
      +019public class BrailleSmokeTest {
      +020
      +021    @Test
      +022    public void smokeTestDatabaseAndGraph() throws Exception {
      +023        Helpers.createFolderHierarchy();
      +024        SqlGenerate.initializeDatabase();
      +025
      +026        int studentId = com.studentgui.apphelpers.Database.getOrCreateStudent("JUnit Smoke Student");
      +027        int ptId = com.studentgui.apphelpers.Database.getOrCreateProgressType("Braille");
      +028
      +029        String[] codes = new String[28];
      +030        int[] scores = new int[28];
      +031        for (int i = 0; i < 28; i++) { codes[i] = "P" + (i+1); scores[i] = (i % 5) + 1; }
      +032        com.studentgui.apphelpers.Database.ensureAssessmentParts(ptId, codes);
      +033        int sessionId = com.studentgui.apphelpers.Database.createProgressSession(studentId, ptId, LocalDate.now());
      +034        com.studentgui.apphelpers.Database.insertAssessmentResults(sessionId, ptId, codes, scores);
      +035
      +036        List<List<Integer>> allSkillValues = com.studentgui.apphelpers.Database.fetchLatestAssessmentResults("JUnit Smoke Student", "Braille", 5);
      +037        assertNotNull(allSkillValues);
      +038
      +039        JLineGraph graph = new JLineGraph();
      +040        graph.updateWithData(allSkillValues);
      +041    }
      +042}
      +
      +
      +
      +
      +
      +
      +
      +
      +
      +
      +
      +
      +
      +
      +
      +
      +
      +
      +
      +
      +
      +
      +
      +
      +
      +
      +
      +
      +
      +
      +
      +
      +
      +
      +
      +
      +
      +
      +
      +
      +
      +
      +
      +
      +
      +
      +
      +
      +
      +
      +
      +
      +
      +
      +
      +
      +
      +
      +
      +
      +
      +
      +
      + + diff --git a/target/site/testapidocs/src-html/com/studentgui/test/DatabaseEdgeCasesTest.html b/target/site/testapidocs/src-html/com/studentgui/test/DatabaseEdgeCasesTest.html new file mode 100644 index 0000000..9c8b1c6 --- /dev/null +++ b/target/site/testapidocs/src-html/com/studentgui/test/DatabaseEdgeCasesTest.html @@ -0,0 +1,136 @@ + + + + +Source code + + + + + + +
      +
      +
      001package com.studentgui.test;
      +002
      +003import static org.junit.jupiter.api.Assertions.*;
      +004
      +005import java.time.LocalDate;
      +006import java.util.List;
      +007
      +008import org.junit.jupiter.api.BeforeAll;
      +009import org.junit.jupiter.api.Test;
      +010
      +011import com.studentgui.apphelpers.Helpers;
      +012import com.studentgui.apphelpers.SqlGenerate;
      +013
      +014public class DatabaseEdgeCasesTest {
      +015
      +016    @BeforeAll
      +017    public static void init() throws Exception {
      +018        Helpers.createFolderHierarchy();
      +019        SqlGenerate.initializeDatabase();
      +020    }
      +021
      +022    @Test
      +023    public void duplicateStudentNamesReturnSameId() throws Exception {
      +024        int a = com.studentgui.apphelpers.Database.getOrCreateStudent("Dup Student");
      +025        int b = com.studentgui.apphelpers.Database.getOrCreateStudent("Dup Student");
      +026        assertEquals(a, b, "Duplicate student names should return the same id");
      +027    }
      +028
      +029    @Test
      +030    public void ensureAssessmentPartsIsIdempotentAndIgnoresUnknownPartsOnInsert() throws Exception {
      +031        int pt = com.studentgui.apphelpers.Database.getOrCreateProgressType("EdgeType");
      +032        String[] parts = new String[] {"X1","X2","X3"};
      +033        com.studentgui.apphelpers.Database.ensureAssessmentParts(pt, parts);
      +034        // calling again should not fail and should be idempotent
      +035        com.studentgui.apphelpers.Database.ensureAssessmentParts(pt, parts);
      +036
      +037        int sid = com.studentgui.apphelpers.Database.getOrCreateStudent("Edge Student");
      +038        int session = com.studentgui.apphelpers.Database.createProgressSession(sid, pt, LocalDate.now());
      +039        // insert with an unknown part code - should be ignored, no exception
      +040        com.studentgui.apphelpers.Database.insertAssessmentResults(session, pt, new String[] {"X1","UNKNOWN"}, new int[] {5, 9});
      +041
      +042        List<List<Integer>> rows = com.studentgui.apphelpers.Database.fetchLatestAssessmentResults("Edge Student", "EdgeType", 5);
      +043        assertNotNull(rows);
      +044        assertTrue(rows.size() >= 1);
      +045    }
      +046
      +047    @Test
      +048    public void saveSessionNotesPersistsNotes() throws Exception {
      +049        int pt = com.studentgui.apphelpers.Database.getOrCreateProgressType("NoteType");
      +050        int sid = com.studentgui.apphelpers.Database.getOrCreateStudent("Notes Student");
      +051        int session = com.studentgui.apphelpers.Database.createProgressSession(sid, pt, LocalDate.now());
      +052        com.studentgui.apphelpers.Database.saveSessionNotes(session, "These are test notes");
      +053
      +054        List<List<Integer>> rows = com.studentgui.apphelpers.Database.fetchLatestAssessmentResults("Notes Student", "NoteType", 5);
      +055        // fetchLatestAssessmentResults doesn't return notes, but we can at least ensure the session exists by getting session rows
      +056        assertNotNull(rows);
      +057    }
      +058}
      +
      +
      +
      +
      +
      +
      +
      +
      +
      +
      +
      +
      +
      +
      +
      +
      +
      +
      +
      +
      +
      +
      +
      +
      +
      +
      +
      +
      +
      +
      +
      +
      +
      +
      +
      +
      +
      +
      +
      +
      +
      +
      +
      +
      +
      +
      +
      +
      +
      +
      +
      +
      +
      +
      +
      +
      +
      +
      +
      +
      +
      +
      +
      + + diff --git a/target/site/testapidocs/src-html/com/studentgui/test/DatabaseTest.html b/target/site/testapidocs/src-html/com/studentgui/test/DatabaseTest.html new file mode 100644 index 0000000..08954c3 --- /dev/null +++ b/target/site/testapidocs/src-html/com/studentgui/test/DatabaseTest.html @@ -0,0 +1,126 @@ + + + + +Source code + + + + + + +
      +
      +
      001package com.studentgui.test;
      +002
      +003import static org.junit.jupiter.api.Assertions.*;
      +004
      +005import java.time.LocalDate;
      +006import java.util.List;
      +007
      +008import org.junit.jupiter.api.BeforeAll;
      +009import org.junit.jupiter.api.Test;
      +010
      +011import com.studentgui.apphelpers.Helpers;
      +012import com.studentgui.apphelpers.SqlGenerate;
      +013
      +014/**
      +015 * Basic integration tests for the Database helper using the on-disk sqlite
      +016 * created in the project's application data folder. These tests are small and
      +017 * intentionally exercise CRUD paths used by the UI pages.
      +018 */
      +019public class DatabaseTest {
      +020
      +021    @BeforeAll
      +022    public static void init() throws Exception {
      +023        Helpers.createFolderHierarchy();
      +024        SqlGenerate.initializeDatabase();
      +025    }
      +026
      +027    @Test
      +028    public void testStudentCreateAndFetch() throws Exception {
      +029        int sid = com.studentgui.apphelpers.Database.getOrCreateStudent("Test Student A");
      +030        assertTrue(sid > 0);
      +031
      +032        int ptId = com.studentgui.apphelpers.Database.getOrCreateProgressType("TestType");
      +033        assertTrue(ptId > 0);
      +034
      +035        String[] parts = new String[] {"P1","P2","P3"};
      +036        com.studentgui.apphelpers.Database.ensureAssessmentParts(ptId, parts);
      +037
      +038        int sessionId = com.studentgui.apphelpers.Database.createProgressSession(sid, ptId, LocalDate.now());
      +039        assertTrue(sessionId > 0);
      +040
      +041        int[] scores = new int[] {1,2,3};
      +042        com.studentgui.apphelpers.Database.insertAssessmentResults(sessionId, ptId, parts, scores);
      +043
      +044        List<List<Integer>> results = com.studentgui.apphelpers.Database.fetchLatestAssessmentResults("Test Student A", "TestType", 5);
      +045        assertNotNull(results);
      +046        assertTrue(results.size() >= 1);
      +047    }
      +048}
      +
      +
      +
      +
      +
      +
      +
      +
      +
      +
      +
      +
      +
      +
      +
      +
      +
      +
      +
      +
      +
      +
      +
      +
      +
      +
      +
      +
      +
      +
      +
      +
      +
      +
      +
      +
      +
      +
      +
      +
      +
      +
      +
      +
      +
      +
      +
      +
      +
      +
      +
      +
      +
      +
      +
      +
      +
      +
      +
      +
      +
      +
      +
      + + diff --git a/target/site/testapidocs/src-html/com/studentgui/test/ExportBrailleReportsTest.html b/target/site/testapidocs/src-html/com/studentgui/test/ExportBrailleReportsTest.html new file mode 100644 index 0000000..787da09 --- /dev/null +++ b/target/site/testapidocs/src-html/com/studentgui/test/ExportBrailleReportsTest.html @@ -0,0 +1,204 @@ + + + + +Source code + + + + + + +
      +
      +
      001package com.studentgui.test;
      +002
      +003import java.lang.reflect.Field;
      +004import java.nio.file.Path;
      +005import java.time.LocalDate;
      +006import java.util.Map;
      +007
      +008import static org.junit.jupiter.api.Assertions.assertTrue;
      +009import org.junit.jupiter.api.Test;
      +010
      +011import com.studentgui.apphelpers.Database;
      +012import com.studentgui.apphelpers.Helpers;
      +013import com.studentgui.apppages.Braille;
      +014import com.studentgui.apppages.JLineGraph;
      +015
      +016/**
      +017 * Test that generates example Braille exports (per-phase PNGs + MD/HTML)
      +018 * for the student "Test Student". This mirrors the export logic used in
      +019 * the Braille page submit handler but runs headlessly as a test so the
      +020 * agent can produce example files for review.
      +021 */
      +022public class ExportBrailleReportsTest {
      +023
      +024    @Test
      +025    public void generateBrailleExport() throws Exception {
      +026        // Force headless mode for chart rendering in CI-like environments
      +027        System.setProperty("java.awt.headless", "true");
      +028
      +029        Helpers.createFolderHierarchy();
      +030        // Ensure DB exists and schema is initialized (idempotent)
      +031        com.studentgui.apphelpers.SqlGenerate.initializeDatabase();
      +032
      +033        String student = "Test Student";
      +034        String progressType = "Braille";
      +035
      +036        // Instantiate the Braille page to ensure canonical parts are created
      +037        JLineGraph graph = new JLineGraph();
      +038        Braille braille = new Braille(student, LocalDate.now(), graph);
      +039
      +040        // Fetch historical rows + dates
      +041        Database.ResultsWithDates rwd = Database.fetchLatestAssessmentResultsWithDates(student, progressType, Integer.MAX_VALUE);
      +042
      +043        // Reflectively obtain the partCodes and human labels from Braille instance
      +044        Field pcField = Braille.class.getDeclaredField("partCodes");
      +045        pcField.setAccessible(true);
      +046        String[] partCodes = (String[]) pcField.get(braille);
      +047
      +048        Field partsField = Braille.class.getDeclaredField("parts");
      +049        partsField.setAccessible(true);
      +050        String[][] parts = (String[][]) partsField.get(braille);
      +051        String[] labels = new String[parts.length];
      +052        for (int i = 0; i < parts.length; i++) labels[i] = parts[i][1];
      +053
      +054        java.nio.file.Path out = Helpers.APP_HOME.resolve("StudentDataFiles").resolve(Helpers.safeName(student)).resolve("plots");
      +055        java.nio.file.Files.createDirectories(out);
      +056        String baseName = "Braille-example-" + java.time.LocalDate.now().toString();
      +057
      +058        if (rwd != null && rwd.rows != null && !rwd.rows.isEmpty()) {
      +059            graph.updateWithGroupedDataByDate(rwd.dates, rwd.rows, partCodes, labels);
      +060        } else {
      +061            // No historical data; create a single-row from zeros by reflection of Braille's fields
      +062            java.util.List<java.util.List<Integer>> rowsList = new java.util.ArrayList<>();
      +063            java.util.List<Integer> latest = new java.util.ArrayList<>();
      +064            for (int i = 0; i < partCodes.length; i++) latest.add(0);
      +065            rowsList.add(latest);
      +066            graph.updateWithGroupedData(rowsList, partCodes);
      +067        }
      +068
      +069        Map<String, Path> groups = graph.saveGroupedCharts(out, baseName, 1000, 240);
      +070
      +071        // Build simple markdown and html reports (reuse palette from JLineGraph)
      +072        StringBuilder md = new StringBuilder();
      +073        md.append("# ").append(student).append(" - ").append(java.time.LocalDate.now().toString()).append("\n\n");
      +074        for (Map.Entry<String, Path> e : groups.entrySet()) {
      +075            md.append("## ").append(e.getKey()).append("\n\n");
      +076            md.append("![](./").append(e.getValue().getFileName().toString()).append(")\n\n");
      +077        }
      +078        java.nio.file.Path mdFile = out.resolve(baseName + ".md");
      +079        java.nio.file.Files.writeString(mdFile, md.toString(), java.nio.charset.StandardCharsets.UTF_8);
      +080
      +081        String[] palette = JLineGraph.PALETTE_HEX;
      +082        java.util.LinkedHashMap<String, java.util.List<Integer>> groupsIdx = new java.util.LinkedHashMap<>();
      +083        for (int i = 0; i < partCodes.length; i++) {
      +084            String code = partCodes[i];
      +085            String grp = code != null && code.contains("_") ? code.split("_")[0] : code;
      +086            groupsIdx.computeIfAbsent(grp, k -> new java.util.ArrayList<>()).add(i);
      +087        }
      +088
      +089        StringBuilder html = new StringBuilder();
      +090        html.append("<!doctype html><html><head><meta charset=\"utf-8\"><title>");
      +091        html.append(student).append(" - ").append(java.time.LocalDate.now().toString()).append("</title>");
      +092        html.append("<style>body{font-family:sans-serif;margin:20px;} img{max-width:100%;height:auto;border:1px solid #ccc;margin-bottom:8px;} .legend{max-height:160px;overflow:auto;border:1px solid #ddd;padding:8px;margin-bottom:24px;} .legend-item{display:flex;align-items:center;gap:8px;padding:4px 0;} .swatch{width:18px;height:12px;border:1px solid #333;display:inline-block}</style>");
      +093        html.append("</head><body>");
      +094        html.append("<h1>").append(student).append(" - ").append(java.time.LocalDate.now().toString()).append("</h1>");
      +095        for (Map.Entry<String, Path> e2 : groups.entrySet()) {
      +096            String grp = e2.getKey();
      +097            String imgName = e2.getValue().getFileName().toString();
      +098            html.append("<h2>").append(grp).append("</h2>");
      +099            html.append("<div class=\"plot\"><img src=\"./").append(imgName).append("\" alt=\"").append(grp).append("\"></div>");
      +100            java.util.List<Integer> idxs = groupsIdx.getOrDefault(grp, new java.util.ArrayList<>());
      +101            html.append("<div class=\"legend\">");
      +102            for (int s = 0; s < idxs.size(); s++) {
      +103                int idx = idxs.get(s);
      +104                String code = partCodes[idx];
      +105                String human = labels[idx];
      +106                String seriesName = code + " - " + human;
      +107                String color = palette[s % palette.length];
      +108                html.append("<div class=\"legend-item\">");
      +109                html.append("<span class=\"swatch\" style=\"background:");
      +110                html.append(color).append(";\"></span>");
      +111                html.append("<div>").append(seriesName).append("</div></div>");
      +112            }
      +113            html.append("</div>");
      +114        }
      +115        html.append("</body></html>");
      +116        java.nio.file.Path htmlFile = out.resolve(baseName + ".html");
      +117        java.nio.file.Files.writeString(htmlFile, html.toString(), java.nio.charset.StandardCharsets.UTF_8);
      +118
      +119        System.out.println("Exported Braille report to: " + out.toAbsolutePath().toString());
      +120        for (Map.Entry<String, Path> e : groups.entrySet()) System.out.println(" - " + e.getKey() + " -> " + e.getValue().getFileName());
      +121        System.out.println("MD: " + mdFile.getFileName() + " HTML: " + htmlFile.getFileName());
      +122
      +123        // Quick assertion to ensure at least one image or the md file exists
      +124        assertTrue(java.nio.file.Files.exists(mdFile));
      +125    }
      +126}
      +
      +
      +
      +
      +
      +
      +
      +
      +
      +
      +
      +
      +
      +
      +
      +
      +
      +
      +
      +
      +
      +
      +
      +
      +
      +
      +
      +
      +
      +
      +
      +
      +
      +
      +
      +
      +
      +
      +
      +
      +
      +
      +
      +
      +
      +
      +
      +
      +
      +
      +
      +
      +
      +
      +
      +
      +
      +
      +
      +
      +
      +
      +
      + + diff --git a/target/site/testapidocs/stylesheet.css b/target/site/testapidocs/stylesheet.css new file mode 100644 index 0000000..f71489f --- /dev/null +++ b/target/site/testapidocs/stylesheet.css @@ -0,0 +1,1272 @@ +/* + * Javadoc style sheet + */ + +@import url('resources/fonts/dejavu.css'); + +/* + * These CSS custom properties (variables) define the core color and font + * properties used in this stylesheet. + */ +:root { + /* body, block and code fonts */ + --body-font-family: 'DejaVu Sans', Arial, Helvetica, sans-serif; + --block-font-family: 'DejaVu Serif', Georgia, "Times New Roman", Times, serif; + --code-font-family: 'DejaVu Sans Mono', monospace; + /* Base font sizes for body and code elements */ + --body-font-size: 14px; + --code-font-size: 14px; + /* Text colors for body and block elements */ + --body-text-color: #353833; + --block-text-color: #474747; + /* Background colors for various structural elements */ + --body-background-color: #ffffff; + --section-background-color: #f8f8f8; + --detail-background-color: #ffffff; + /* Colors for navigation bar and table captions */ + --navbar-background-color: #4D7A97; + --navbar-text-color: #ffffff; + /* Background color for subnavigation and various headers */ + --subnav-background-color: #dee3e9; + /* Background and text colors for selected tabs and navigation items */ + --selected-background-color: #f8981d; + --selected-text-color: #253441; + --selected-link-color: #1f389c; + /* Background colors for generated tables */ + --even-row-color: #ffffff; + --odd-row-color: #eeeeef; + /* Text color for page title */ + --title-color: #2c4557; + /* Text colors for links */ + --link-color: #4A6782; + --link-color-active: #bb7a2a; + /* Snippet colors */ + --snippet-background-color: #ebecee; + --snippet-text-color: var(--block-text-color); + --snippet-highlight-color: #f7c590; + /* Border colors for structural elements and user defined tables */ + --border-color: #ededed; + --table-border-color: #000000; + /* Search input colors */ + --search-input-background-color: #ffffff; + --search-input-text-color: #000000; + --search-input-placeholder-color: #909090; + /* Highlight color for active search tag target */ + --search-tag-highlight-color: #ffff00; + /* Adjustments for icon and active background colors of copy-to-clipboard buttons */ + --copy-icon-brightness: 100%; + --copy-button-background-color-active: rgba(168, 168, 176, 0.3); + /* Colors for invalid tag notifications */ + --invalid-tag-background-color: #ffe6e6; + --invalid-tag-text-color: #000000; +} +/* + * Styles for individual HTML elements. + * + * These are styles that are specific to individual HTML elements. Changing them affects the style of a particular + * HTML element throughout the page. + */ +body { + background-color:var(--body-background-color); + color:var(--body-text-color); + font-family:var(--body-font-family); + font-size:var(--body-font-size); + margin:0; + padding:0; + height:100%; + width:100%; +} +iframe { + margin:0; + padding:0; + height:100%; + width:100%; + overflow-y:scroll; + border:none; +} +a:link, a:visited { + text-decoration:none; + color:var(--link-color); +} +a[href]:hover, a[href]:focus { + text-decoration:none; + color:var(--link-color-active); +} +pre { + font-family:var(--code-font-family); + font-size:1em; +} +h1 { + font-size:1.428em; +} +h2 { + font-size:1.285em; +} +h3 { + font-size:1.14em; +} +h4 { + font-size:1.072em; +} +h5 { + font-size:1.001em; +} +h6 { + font-size:0.93em; +} +/* Disable font boosting for selected elements */ +h1, h2, h3, h4, h5, h6, div.member-signature { + max-height: 1000em; +} +ul { + list-style-type:disc; +} +code, tt { + font-family:var(--code-font-family); +} +:not(h1, h2, h3, h4, h5, h6) > code, +:not(h1, h2, h3, h4, h5, h6) > tt { + font-size:var(--code-font-size); + padding-top:4px; + margin-top:8px; + line-height:1.4em; +} +dt code { + font-family:var(--code-font-family); + font-size:1em; + padding-top:4px; +} +.summary-table dt code { + font-family:var(--code-font-family); + font-size:1em; + vertical-align:top; + padding-top:4px; +} +sup { + font-size:8px; +} +button { + font-family: var(--body-font-family); + font-size: 1em; +} +/* + * Styles for HTML generated by javadoc. + * + * These are style classes that are used by the standard doclet to generate HTML documentation. + */ + +/* + * Styles for document title and copyright. + */ +.about-language { + float:right; + padding:0 21px 8px 8px; + font-size:0.915em; + margin-top:-9px; + height:2.9em; +} +.legal-copy { + margin-left:.5em; +} +/* + * Styles for navigation bar. + */ +@media screen { + div.flex-box { + position:fixed; + display:flex; + flex-direction:column; + height: 100%; + width: 100%; + } + header.flex-header { + flex: 0 0 auto; + } + div.flex-content { + flex: 1 1 auto; + overflow-y: auto; + } +} +.top-nav { + background-color:var(--navbar-background-color); + color:var(--navbar-text-color); + float:left; + width:100%; + clear:right; + min-height:2.8em; + padding:10px 0 0 0; + overflow:hidden; + font-size:0.857em; +} +button#navbar-toggle-button { + display:none; +} +ul.sub-nav-list-small { + display: none; +} +.sub-nav { + background-color:var(--subnav-background-color); + float:left; + width:100%; + overflow:hidden; + font-size:0.857em; +} +.sub-nav div { + clear:left; + float:left; + padding:6px; + text-transform:uppercase; +} +.sub-nav .sub-nav-list { + padding-top:4px; +} +ul.nav-list { + display:block; + margin:0 25px 0 0; + padding:0; +} +ul.sub-nav-list { + float:left; + margin:0 25px 0 0; + padding:0; +} +ul.nav-list li { + list-style:none; + float:left; + padding: 5px 6px; + text-transform:uppercase; +} +.sub-nav .nav-list-search { + float:right; + margin:0; + padding:6px; + clear:none; + text-align:right; + position:relative; +} +ul.sub-nav-list li { + list-style:none; + float:left; +} +.top-nav a:link, .top-nav a:active, .top-nav a:visited { + color:var(--navbar-text-color); + text-decoration:none; + text-transform:uppercase; +} +.top-nav a:hover { + color:var(--link-color-active); +} +.nav-bar-cell1-rev { + background-color:var(--selected-background-color); + color:var(--selected-text-color); + margin: auto 5px; +} +.skip-nav { + position:absolute; + top:auto; + left:-9999px; + overflow:hidden; +} +/* + * Hide navigation links and search box in print layout + */ +@media print { + ul.nav-list, div.sub-nav { + display:none; + } +} +/* + * Styles for page header. + */ +.title { + color:var(--title-color); + margin:10px 0; +} +.sub-title { + margin:5px 0 0 0; +} +ul.contents-list { + margin: 0 0 15px 0; + padding: 0; + list-style: none; +} +ul.contents-list li { + font-size:0.93em; +} +/* + * Styles for headings. + */ +body.class-declaration-page .summary h2, +body.class-declaration-page .details h2, +body.class-use-page h2, +body.module-declaration-page .block-list h2 { + font-style: italic; + padding:0; + margin:15px 0; +} +body.class-declaration-page .summary h3, +body.class-declaration-page .details h3, +body.class-declaration-page .summary .inherited-list h2 { + background-color:var(--subnav-background-color); + border:1px solid var(--border-color); + margin:0 0 6px -8px; + padding:7px 5px; +} +/* + * Styles for page layout containers. + */ +main { + clear:both; + padding:10px 20px; + position:relative; +} +dl.notes > dt { + font-family: var(--body-font-family); + font-size:0.856em; + font-weight:bold; + margin:10px 0 0 0; + color:var(--body-text-color); +} +dl.notes > dd { + margin:5px 10px 10px 0; + font-size:1em; + font-family:var(--block-font-family) +} +dl.name-value > dt { + margin-left:1px; + font-size:1.1em; + display:inline; + font-weight:bold; +} +dl.name-value > dd { + margin:0 0 0 1px; + font-size:1.1em; + display:inline; +} +/* + * Styles for lists. + */ +li.circle { + list-style:circle; +} +ul.horizontal li { + display:inline; + font-size:0.9em; +} +div.inheritance { + margin:0; + padding:0; +} +div.inheritance div.inheritance { + margin-left:2em; +} +ul.block-list, +ul.details-list, +ul.member-list, +ul.summary-list { + margin:10px 0 10px 0; + padding:0; +} +ul.block-list > li, +ul.details-list > li, +ul.member-list > li, +ul.summary-list > li { + list-style:none; + margin-bottom:15px; + line-height:1.4; +} +ul.ref-list { + padding:0; + margin:0; +} +ul.ref-list > li { + list-style:none; +} +.summary-table dl, .summary-table dl dt, .summary-table dl dd { + margin-top:0; + margin-bottom:1px; +} +ul.tag-list, ul.tag-list-long { + padding-left: 0; + list-style: none; +} +ul.tag-list li { + display: inline; +} +ul.tag-list li:not(:last-child):after, +ul.tag-list-long li:not(:last-child):after +{ + content: ", "; + white-space: pre-wrap; +} +ul.preview-feature-list { + list-style: none; + margin:0; + padding:0.1em; + line-height: 1.6em; +} +/* + * Styles for tables. + */ +.summary-table, .details-table { + width:100%; + border-spacing:0; + border:1px solid var(--border-color); + border-top:0; + padding:0; +} +.caption { + position:relative; + text-align:left; + background-repeat:no-repeat; + color:var(--selected-text-color); + clear:none; + overflow:hidden; + padding: 10px 0 0 1px; + margin:0; +} +.caption a:link, .caption a:visited { + color:var(--selected-link-color); +} +.caption a:hover, +.caption a:active { + color:var(--navbar-text-color); +} +.caption span { + font-weight:bold; + white-space:nowrap; + padding:5px 12px 7px 12px; + display:inline-block; + float:left; + background-color:var(--selected-background-color); + border: none; + height:16px; +} +div.table-tabs { + padding:10px 0 0 1px; + margin:10px 0 0 0; +} +div.table-tabs > button { + border: none; + cursor: pointer; + padding: 5px 12px 7px 12px; + font-weight: bold; + margin-right: 8px; +} +div.table-tabs > .active-table-tab { + background: var(--selected-background-color); + color: var(--selected-text-color); +} +div.table-tabs > button.table-tab { + background: var(--navbar-background-color); + color: var(--navbar-text-color); +} +.two-column-search-results { + display: grid; + grid-template-columns: minmax(400px, max-content) minmax(400px, auto); +} +div.checkboxes { + line-height: 2em; +} +div.checkboxes > span { + margin-left: 10px; +} +div.checkboxes > label { + margin-left: 8px; + white-space: nowrap; +} +div.checkboxes > label > input { + margin: 0 2px; +} +.two-column-summary { + display: grid; + grid-template-columns: minmax(25%, max-content) minmax(25%, auto); +} +.three-column-summary { + display: grid; + grid-template-columns: minmax(15%, max-content) minmax(20%, max-content) minmax(20%, auto); +} +.three-column-release-summary { + display: grid; + grid-template-columns: minmax(40%, max-content) minmax(10%, max-content) minmax(40%, auto); +} +.four-column-summary { + display: grid; + grid-template-columns: minmax(10%, max-content) minmax(15%, max-content) minmax(15%, max-content) minmax(15%, auto); +} +@media screen and (max-width: 1000px) { + .four-column-summary { + display: grid; + grid-template-columns: minmax(15%, max-content) minmax(15%, auto); + } +} +@media screen and (max-width: 800px) { + .two-column-search-results { + display: grid; + grid-template-columns: minmax(40%, max-content) minmax(40%, auto); + } + .three-column-summary { + display: grid; + grid-template-columns: minmax(10%, max-content) minmax(25%, auto); + } + .three-column-release-summary { + display: grid; + grid-template-columns: minmax(70%, max-content) minmax(30%, max-content) + } + .three-column-summary .col-last, + .three-column-release-summary .col-last{ + grid-column-end: span 2; + } +} +@media screen and (max-width: 600px) { + .two-column-summary { + display: grid; + grid-template-columns: 1fr; + } +} +.summary-table > div, .details-table > div { + text-align:left; + padding: 8px 3px 3px 7px; + overflow-x: auto; + scrollbar-width: thin; +} +.col-first, .col-second, .col-last, .col-constructor-name, .col-summary-item-name { + vertical-align:top; + padding-right:0; + padding-top:8px; + padding-bottom:3px; +} +.table-header { + background:var(--subnav-background-color); + font-weight: bold; +} +/* Sortable table columns */ +.table-header[onclick] { + cursor: pointer; +} +.table-header[onclick]::after { + content:""; + display:inline-block; + background-image:url('data:image/svg+xml; utf8, \ + \ + '); + background-size:100% 100%; + width:9px; + height:14px; + margin-left:4px; + margin-bottom:-3px; +} +.table-header[onclick].sort-asc::after { + background-image:url('data:image/svg+xml; utf8, \ + \ + \ + '); + +} +.table-header[onclick].sort-desc::after { + background-image:url('data:image/svg+xml; utf8, \ + \ + \ + '); +} +.col-first, .col-first { + font-size:0.93em; +} +.col-second, .col-second, .col-last, .col-constructor-name, .col-summary-item-name, .col-last { + font-size:0.93em; +} +.col-first, .col-second, .col-constructor-name { + vertical-align:top; + overflow: auto; +} +.col-last { + white-space:normal; +} +.col-first a:link, .col-first a:visited, +.col-second a:link, .col-second a:visited, +.col-first a:link, .col-first a:visited, +.col-second a:link, .col-second a:visited, +.col-constructor-name a:link, .col-constructor-name a:visited, +.col-summary-item-name a:link, .col-summary-item-name a:visited { + font-weight:bold; +} +.even-row-color, .even-row-color .table-header { + background-color:var(--even-row-color); +} +.odd-row-color, .odd-row-color .table-header { + background-color:var(--odd-row-color); +} +/* + * Styles for contents. + */ +div.block { + font-size:var(--body-font-size); + font-family:var(--block-font-family); +} +.col-last div { + padding-top:0; +} +.col-last a { + padding-bottom:3px; +} +.module-signature, +.package-signature, +.type-signature, +.member-signature { + font-family:var(--code-font-family); + font-size:1em; + margin:14px 0; + white-space: pre-wrap; +} +.module-signature, +.package-signature, +.type-signature { + margin-top: 0; +} +.member-signature .type-parameters-long, +.member-signature .parameters, +.member-signature .exceptions { + display: inline-block; + vertical-align: top; + white-space: pre; +} +.member-signature .type-parameters { + white-space: normal; +} +/* + * Styles for formatting effect. + */ +.source-line-no { + /* Color of line numbers in source pages can be set via custom property below */ + color:var(--source-linenumber-color, green); + padding:0 30px 0 0; +} +.block { + display:block; + margin:0 10px 5px 0; + color:var(--block-text-color); +} +.deprecated-label, .description-from-type-label, .implementation-label, .member-name-link, +.module-label-in-package, .module-label-in-type, .package-label-in-type, +.package-hierarchy-label, .type-name-label, .type-name-link, .search-tag-link, .preview-label { + font-weight:bold; +} +.deprecation-comment, .help-footnote, .preview-comment { + font-style:italic; +} +.deprecation-block { + font-size:1em; + font-family:var(--block-font-family); + border-style:solid; + border-width:thin; + border-radius:10px; + padding:10px; + margin-bottom:10px; + margin-right:10px; + display:inline-block; +} +.preview-block { + font-size:1em; + font-family:var(--block-font-family); + border-style:solid; + border-width:thin; + border-radius:10px; + padding:10px; + margin-bottom:10px; + margin-right:10px; + display:inline-block; +} +div.block div.deprecation-comment { + font-style:normal; +} +details.invalid-tag, span.invalid-tag { + font-size:1em; + font-family:var(--block-font-family); + color: var(--invalid-tag-text-color); + background: var(--invalid-tag-background-color); + border: thin solid var(--table-border-color); + border-radius:2px; + padding: 2px 4px; + display:inline-block; +} +details summary { + cursor: pointer; +} +/* + * Styles specific to HTML5 elements. + */ +main, nav, header, footer, section { + display:block; +} +/* + * Styles for javadoc search. + */ +.ui-state-active { + /* Overrides the color of selection used in jQuery UI */ + background: var(--selected-background-color); + border: 1px solid var(--selected-background-color); + color: var(--selected-text-color); +} +.ui-autocomplete-category { + font-weight:bold; + font-size:15px; + padding:7px 0 7px 3px; + background-color:var(--navbar-background-color); + color:var(--navbar-text-color); +} +.ui-autocomplete { + max-height:85%; + max-width:65%; + overflow-y:auto; + overflow-x:auto; + scrollbar-width: thin; + white-space:nowrap; + box-shadow: 0 3px 6px rgba(0,0,0,0.16), 0 3px 6px rgba(0,0,0,0.23); +} +ul.ui-autocomplete { + position:fixed; + z-index:1; + background-color: var(--body-background-color); +} +ul.ui-autocomplete li { + float:left; + clear:both; + min-width:100%; +} +ul.ui-autocomplete li.ui-static-link { + position:sticky; + bottom:0; + left:0; + background: var(--subnav-background-color); + padding: 5px 0; + font-family: var(--body-font-family); + font-size: 0.93em; + font-weight: bolder; + z-index: 2; +} +li.ui-static-link a, li.ui-static-link a:visited { + text-decoration:none; + color:var(--link-color); + float:right; + margin-right:20px; +} +.ui-autocomplete .result-item { + font-size: inherit; +} +.ui-autocomplete .result-highlight { + font-weight:bold; +} +#search-input, #page-search-input { + background-image:url('resources/glass.png'); + background-size:13px; + background-repeat:no-repeat; + background-position:2px 3px; + background-color: var(--search-input-background-color); + color: var(--search-input-text-color); + border-color: var(--border-color); + padding-left:20px; + width: 250px; + margin: 0; +} +#search-input { + margin-left: 4px; +} +#reset-button { + background-color: transparent; + background-image:url('resources/x.png'); + background-repeat:no-repeat; + background-size:contain; + border:0; + border-radius:0; + width:12px; + height:12px; + position:absolute; + right:12px; + top:10px; + font-size:0; +} +::placeholder { + color:var(--search-input-placeholder-color); + opacity: 1; +} +.search-tag-desc-result { + font-style:italic; + font-size:11px; +} +.search-tag-holder-result { + font-style:italic; + font-size:12px; +} +.search-tag-result:target { + background-color:var(--search-tag-highlight-color); +} +details.page-search-details { + display: inline-block; +} +div#result-container { + font-size: 1em; +} +div#result-container a.search-result-link { + padding: 0; + margin: 4px 0; + width: 100%; +} +#result-container .result-highlight { + font-weight:bolder; +} +.page-search-info { + background-color: var(--subnav-background-color); + border-radius: 3px; + border: 0 solid var(--border-color); + padding: 0 8px; + overflow: hidden; + height: 0; + transition: all 0.2s ease; +} +div.table-tabs > button.table-tab { + background: var(--navbar-background-color); + color: var(--navbar-text-color); +} +.page-search-header { + padding: 5px 12px 7px 12px; + font-weight: bold; + margin-right: 3px; + background-color:var(--navbar-background-color); + color:var(--navbar-text-color); + display: inline-block; +} +button.page-search-header { + border: none; + cursor: pointer; +} +span#page-search-link { + text-decoration: underline; +} +.module-graph span, .sealed-graph span { + display:none; + position:absolute; +} +.module-graph:hover span, .sealed-graph:hover span { + display:block; + margin: -100px 0 0 100px; + z-index: 1; +} +.inherited-list { + margin: 10px 0 10px 0; +} +section.class-description { + line-height: 1.4; +} +.summary section[class$="-summary"], .details section[class$="-details"], +.class-uses .detail, .serialized-class-details { + padding: 0 20px 5px 10px; + border: 1px solid var(--border-color); + background-color: var(--section-background-color); +} +.inherited-list, section[class$="-details"] .detail { + padding:0 0 5px 8px; + background-color:var(--detail-background-color); + border:none; +} +.vertical-separator { + padding: 0 5px; +} +ul.help-section-list { + margin: 0; +} +ul.help-subtoc > li { + display: inline-block; + padding-right: 5px; + font-size: smaller; +} +ul.help-subtoc > li::before { + content: "\2022" ; + padding-right:2px; +} +.help-note { + font-style: italic; +} +/* + * Indicator icon for external links. + */ +main a[href*="://"]::after { + content:""; + display:inline-block; + background-image:url('data:image/svg+xml; utf8, \ + \ + \ + '); + background-size:100% 100%; + width:7px; + height:7px; + margin-left:2px; + margin-bottom:4px; +} +main a[href*="://"]:hover::after, +main a[href*="://"]:focus::after { + background-image:url('data:image/svg+xml; utf8, \ + \ + \ + '); +} +/* + * Styles for header/section anchor links + */ +a.anchor-link { + opacity: 0; + transition: opacity 0.1s; +} +:hover > a.anchor-link { + opacity: 80%; +} +a.anchor-link:hover, +a.anchor-link:focus-visible, +a.anchor-link.visible { + opacity: 100%; +} +a.anchor-link > img { + width: 0.9em; + height: 0.9em; +} +/* + * Styles for copy-to-clipboard buttons + */ +button.copy { + opacity: 70%; + border: none; + border-radius: 3px; + position: relative; + background:none; + transition: opacity 0.3s; + cursor: pointer; +} +:hover > button.copy { + opacity: 80%; +} +button.copy:hover, +button.copy:active, +button.copy:focus-visible, +button.copy.visible { + opacity: 100%; +} +button.copy img { + position: relative; + background: none; + filter: brightness(var(--copy-icon-brightness)); +} +button.copy:active { + background-color: var(--copy-button-background-color-active); +} +button.copy span { + color: var(--body-text-color); + position: relative; + top: -0.1em; + transition: all 0.1s; + font-size: 0.76rem; + line-height: 1.2em; + opacity: 0; +} +button.copy:hover span, +button.copy:focus-visible span, +button.copy.visible span { + opacity: 100%; +} +/* search page copy button */ +button#page-search-copy { + margin-left: 0.4em; + padding:0.3em; + top:0.13em; +} +button#page-search-copy img { + width: 1.2em; + height: 1.2em; + padding: 0.01em 0; + top: 0.15em; +} +button#page-search-copy span { + color: var(--body-text-color); + line-height: 1.2em; + padding: 0.2em; + top: -0.18em; +} +div.page-search-info:hover button#page-search-copy span { + opacity: 100%; +} +/* snippet copy button */ +button.snippet-copy { + position: absolute; + top: 6px; + right: 6px; + height: 1.7em; + padding: 2px; +} +button.snippet-copy img { + width: 18px; + height: 18px; + padding: 0.05em 0; +} +button.snippet-copy span { + line-height: 1.2em; + padding: 0.2em; + position: relative; + top: -0.5em; +} +div.snippet-container:hover button.snippet-copy span { + opacity: 100%; +} +/* + * Styles for user-provided tables. + * + * borderless: + * No borders, vertical margins, styled caption. + * This style is provided for use with existing doc comments. + * In general, borderless tables should not be used for layout purposes. + * + * plain: + * Plain borders around table and cells, vertical margins, styled caption. + * Best for small tables or for complex tables for tables with cells that span + * rows and columns, when the "striped" style does not work well. + * + * striped: + * Borders around the table and vertical borders between cells, striped rows, + * vertical margins, styled caption. + * Best for tables that have a header row, and a body containing a series of simple rows. + */ + +table.borderless, +table.plain, +table.striped { + margin-top: 10px; + margin-bottom: 10px; +} +table.borderless > caption, +table.plain > caption, +table.striped > caption { + font-weight: bold; + font-size: smaller; +} +table.borderless th, table.borderless td, +table.plain th, table.plain td, +table.striped th, table.striped td { + padding: 2px 5px; +} +table.borderless, +table.borderless > thead > tr > th, table.borderless > tbody > tr > th, table.borderless > tr > th, +table.borderless > thead > tr > td, table.borderless > tbody > tr > td, table.borderless > tr > td { + border: none; +} +table.borderless > thead > tr, table.borderless > tbody > tr, table.borderless > tr { + background-color: transparent; +} +table.plain { + border-collapse: collapse; + border: 1px solid var(--table-border-color); +} +table.plain > thead > tr, table.plain > tbody tr, table.plain > tr { + background-color: transparent; +} +table.plain > thead > tr > th, table.plain > tbody > tr > th, table.plain > tr > th, +table.plain > thead > tr > td, table.plain > tbody > tr > td, table.plain > tr > td { + border: 1px solid var(--table-border-color); +} +table.striped { + border-collapse: collapse; + border: 1px solid var(--table-border-color); +} +table.striped > thead { + background-color: var(--subnav-background-color); +} +table.striped > thead > tr > th, table.striped > thead > tr > td { + border: 1px solid var(--table-border-color); +} +table.striped > tbody > tr:nth-child(even) { + background-color: var(--odd-row-color) +} +table.striped > tbody > tr:nth-child(odd) { + background-color: var(--even-row-color) +} +table.striped > tbody > tr > th, table.striped > tbody > tr > td { + border-left: 1px solid var(--table-border-color); + border-right: 1px solid var(--table-border-color); +} +table.striped > tbody > tr > th { + font-weight: normal; +} +/** + * Tweak style for small screens. + */ +@media screen and (max-width: 920px) { + header.flex-header { + max-height: 100vh; + overflow-y: auto; + } + div#navbar-top { + height: 2.8em; + transition: height 0.35s ease; + } + ul.nav-list { + display: block; + width: 40%; + float:left; + clear: left; + margin: 10px 0 0 0; + padding: 0; + } + ul.nav-list li { + float: none; + padding: 6px; + margin-left: 10px; + margin-top: 2px; + } + ul.sub-nav-list-small { + display:block; + height: 100%; + width: 50%; + float: right; + clear: right; + background-color: var(--subnav-background-color); + color: var(--body-text-color); + margin: 6px 0 0 0; + padding: 0; + } + ul.sub-nav-list-small ul { + padding-left: 20px; + } + ul.sub-nav-list-small a:link, ul.sub-nav-list-small a:visited { + color:var(--link-color); + } + ul.sub-nav-list-small a:hover { + color:var(--link-color-active); + } + ul.sub-nav-list-small li { + list-style:none; + float:none; + padding: 6px; + margin-top: 1px; + text-transform:uppercase; + } + ul.sub-nav-list-small > li { + margin-left: 10px; + } + ul.sub-nav-list-small li p { + margin: 5px 0; + } + div#navbar-sub-list { + display: none; + } + .top-nav a:link, .top-nav a:active, .top-nav a:visited { + display: block; + } + button#navbar-toggle-button { + width: 3.4em; + height: 2.8em; + background-color: transparent; + display: block; + float: left; + border: 0; + margin: 0 10px; + cursor: pointer; + font-size: 10px; + } + button#navbar-toggle-button .nav-bar-toggle-icon { + display: block; + width: 24px; + height: 3px; + margin: 1px 0 4px 0; + border-radius: 2px; + transition: all 0.1s; + background-color: var(--navbar-text-color); + } + button#navbar-toggle-button.expanded span.nav-bar-toggle-icon:nth-child(1) { + transform: rotate(45deg); + transform-origin: 10% 10%; + width: 26px; + } + button#navbar-toggle-button.expanded span.nav-bar-toggle-icon:nth-child(2) { + opacity: 0; + } + button#navbar-toggle-button.expanded span.nav-bar-toggle-icon:nth-child(3) { + transform: rotate(-45deg); + transform-origin: 10% 90%; + width: 26px; + } +} +@media screen and (max-width: 800px) { + .about-language { + padding-right: 16px; + } + ul.nav-list li { + margin-left: 5px; + } + ul.sub-nav-list-small > li { + margin-left: 5px; + } + main { + padding: 10px; + } + .summary section[class$="-summary"], .details section[class$="-details"], + .class-uses .detail, .serialized-class-details { + padding: 0 8px 5px 8px; + } + body { + -webkit-text-size-adjust: none; + } +} +@media screen and (max-width: 400px) { + .about-language { + font-size: 10px; + padding-right: 12px; + } +} +@media screen and (max-width: 400px) { + .nav-list-search { + width: 94%; + } + #search-input, #page-search-input { + width: 70%; + } +} +@media screen and (max-width: 320px) { + .nav-list-search > label { + display: none; + } + .nav-list-search { + width: 90%; + } + #search-input, #page-search-input { + width: 80%; + } +} + +pre.snippet { + background-color: var(--snippet-background-color); + color: var(--snippet-text-color); + padding: 10px; + margin: 12px 0; + overflow: auto; + white-space: pre; +} +div.snippet-container { + position: relative; +} +@media screen and (max-width: 800px) { + pre.snippet { + padding-top: 26px; + } + button.snippet-copy { + top: 4px; + right: 4px; + } +} +pre.snippet .italic { + font-style: italic; +} +pre.snippet .bold { + font-weight: bold; +} +pre.snippet .highlighted { + background-color: var(--snippet-highlight-color); + border-radius: 10%; +} diff --git a/target/site/testapidocs/tag-search-index.js b/target/site/testapidocs/tag-search-index.js new file mode 100644 index 0000000..0367dae --- /dev/null +++ b/target/site/testapidocs/tag-search-index.js @@ -0,0 +1 @@ +tagSearchIndex = [];updateSearchResults(); \ No newline at end of file diff --git a/target/site/testapidocs/type-search-index.js b/target/site/testapidocs/type-search-index.js new file mode 100644 index 0000000..6d671df --- /dev/null +++ b/target/site/testapidocs/type-search-index.js @@ -0,0 +1 @@ +typeSearchIndex = [{"l":"All Classes and Interfaces","u":"allclasses-index.html"},{"p":"com.studentgui.test","l":"BrailleDatabaseTest"},{"p":"com.studentgui.test","l":"BrailleSmokeTest"},{"p":"com.studentgui.apphelpers","l":"DatabaseContactLogTest"},{"p":"com.studentgui.test","l":"DatabaseEdgeCasesTest"},{"p":"com.studentgui.test","l":"DatabaseTest"},{"p":"com.studentgui.test","l":"ExportBrailleReportsTest"},{"p":"com.studentgui.apppages","l":"JLineGraphDeterministicJitterTest"},{"p":"com.studentgui.apphelpers","l":"SessionJsonWriterTest"},{"p":"com.studentgui.apphelpers","l":"SqlGenerateTest"}];updateSearchResults(); \ No newline at end of file diff --git a/target/surefire-reports/2025-10-29T13-28-26_658.dumpstream b/target/surefire-reports/2025-10-29T13-28-26_658.dumpstream deleted file mode 100644 index 4de54c3..0000000 --- a/target/surefire-reports/2025-10-29T13-28-26_658.dumpstream +++ /dev/null @@ -1,5 +0,0 @@ -# Created at 2025-10-29T13:28:26.963 -Boot Manifest-JAR contains absolute paths in classpath 'D:\GitHubRepos\StudentGUI_java\target\test-classes' -Hint: -Djdk.net.URLClassPath.disableClassPathURLCheck=true -'other' has different root - diff --git a/target/surefire-reports/TEST-com.studentgui.apphelpers.DatabaseContactLogTest.xml b/target/surefire-reports/TEST-com.studentgui.apphelpers.DatabaseContactLogTest.xml index 8ca0990..91d50bd 100644 --- a/target/surefire-reports/TEST-com.studentgui.apphelpers.DatabaseContactLogTest.xml +++ b/target/surefire-reports/TEST-com.studentgui.apphelpers.DatabaseContactLogTest.xml @@ -1,92 +1,59 @@ - + - - - - - - + + - - - - - - + + + - - - + + - + - + - - - - - - - + + + + + - - - - - - - - - - - - + + + + - - - - + + + - - - - - - - - - - + + + + + + + - - - - - - + + - - - - - + + - - - + \ No newline at end of file diff --git a/target/surefire-reports/TEST-com.studentgui.apphelpers.SessionJsonWriterTest.xml b/target/surefire-reports/TEST-com.studentgui.apphelpers.SessionJsonWriterTest.xml index 324a845..d9540ca 100644 --- a/target/surefire-reports/TEST-com.studentgui.apphelpers.SessionJsonWriterTest.xml +++ b/target/surefire-reports/TEST-com.studentgui.apphelpers.SessionJsonWriterTest.xml @@ -1,91 +1,59 @@ - + - - - - - - + + - - - - - - + + + - - - + + - + - + - - - - - - - + + + + + - - - - - - - - - - - - + + + + - - - - + + + - - - - - - - - - - + + + + + + + - - - - - - + + - - - - - + + - - - + \ No newline at end of file diff --git a/target/surefire-reports/TEST-com.studentgui.apphelpers.SqlGenerateTest.xml b/target/surefire-reports/TEST-com.studentgui.apphelpers.SqlGenerateTest.xml index 7dca299..828892e 100644 --- a/target/surefire-reports/TEST-com.studentgui.apphelpers.SqlGenerateTest.xml +++ b/target/surefire-reports/TEST-com.studentgui.apphelpers.SqlGenerateTest.xml @@ -1,92 +1,59 @@ - + - - - - - - + + - - - - - - + + + - - - + + - + - + - - - - - - - + + + + + - - - - - - - - - - - - + + + + - - - - + + + - - - - - - - - - - + + + + + + + - - - - - - + + - - - - - + + - - - + \ No newline at end of file diff --git a/target/surefire-reports/TEST-com.studentgui.apppages.JLineGraphDeterministicJitterTest.xml b/target/surefire-reports/TEST-com.studentgui.apppages.JLineGraphDeterministicJitterTest.xml index 6016a70..b55bbe3 100644 --- a/target/surefire-reports/TEST-com.studentgui.apppages.JLineGraphDeterministicJitterTest.xml +++ b/target/surefire-reports/TEST-com.studentgui.apppages.JLineGraphDeterministicJitterTest.xml @@ -1,91 +1,60 @@ - + - - - - - + - + - - - - - - + + + - - - + + - + - + - - - - - - - - + + + + + - - - - - - - - - - - - - + + + + - - - - + + + - - - - - - - - - - + + + + + + + - - - - - - + + - - - - - + + - + \ No newline at end of file diff --git a/target/surefire-reports/TEST-com.studentgui.test.BrailleDatabaseTest.xml b/target/surefire-reports/TEST-com.studentgui.test.BrailleDatabaseTest.xml index 511158b..cf727fb 100644 --- a/target/surefire-reports/TEST-com.studentgui.test.BrailleDatabaseTest.xml +++ b/target/surefire-reports/TEST-com.studentgui.test.BrailleDatabaseTest.xml @@ -1,95 +1,60 @@ - + - - - - - + - + - - - - - - + + + - - - + + - + - + - - - - - - - - + + + + + - - - - - - - - - - - - - + + + + - - - - + + + - - - - - - - - - - + + + + + + + - - - - - - + + - - - - - + + - - - + \ No newline at end of file diff --git a/target/surefire-reports/TEST-com.studentgui.test.BrailleSmokeTest.xml b/target/surefire-reports/TEST-com.studentgui.test.BrailleSmokeTest.xml index bfc21d7..b42788b 100644 --- a/target/surefire-reports/TEST-com.studentgui.test.BrailleSmokeTest.xml +++ b/target/surefire-reports/TEST-com.studentgui.test.BrailleSmokeTest.xml @@ -1,96 +1,60 @@ - + - - - - - + - + - - - - - - + + + - - - + + - + - + - - - - - - - - + + + + + - - - - - - - - - - - - - + + + + - - - - + + + - - - - - - - - - - + + + + + + + - - - - - - + + - - - - - + + - - - + \ No newline at end of file diff --git a/target/surefire-reports/TEST-com.studentgui.test.DatabaseEdgeCasesTest.xml b/target/surefire-reports/TEST-com.studentgui.test.DatabaseEdgeCasesTest.xml index 1811893..830e43f 100644 --- a/target/surefire-reports/TEST-com.studentgui.test.DatabaseEdgeCasesTest.xml +++ b/target/surefire-reports/TEST-com.studentgui.test.DatabaseEdgeCasesTest.xml @@ -1,97 +1,62 @@ - + - - - - - + - + - - - - - - + + + - - - + + - + - + - - - - - - - - + + + + + - - - - - - - - - - - - - + + + + - - - - + + + - - - - - - - - - - + + + + + + + - - - - - - + + - - - - - + + - - - - - + + + \ No newline at end of file diff --git a/target/surefire-reports/TEST-com.studentgui.test.DatabaseTest.xml b/target/surefire-reports/TEST-com.studentgui.test.DatabaseTest.xml index e3c3cfc..c4f362c 100644 --- a/target/surefire-reports/TEST-com.studentgui.test.DatabaseTest.xml +++ b/target/surefire-reports/TEST-com.studentgui.test.DatabaseTest.xml @@ -1,95 +1,60 @@ - + - - - - - + - + - - - - - - + + + - - - + + - + - + - - - - - - - - + + + + + - - - - - - - - - - - - - + + + + - - - - + + + - - - - - - - - - - + + + + + + + - - - - - - + + - - - - - + + - - - + \ No newline at end of file diff --git a/target/surefire-reports/TEST-com.studentgui.test.ExportBrailleReportsTest.xml b/target/surefire-reports/TEST-com.studentgui.test.ExportBrailleReportsTest.xml index 1c25e65..5c735ca 100644 --- a/target/surefire-reports/TEST-com.studentgui.test.ExportBrailleReportsTest.xml +++ b/target/surefire-reports/TEST-com.studentgui.test.ExportBrailleReportsTest.xml @@ -1,110 +1,73 @@ - + - - - - - + - + - - - - - - + + + - - - + + - + - + - - - - - - - - + + + + + - - - - - - - - - - - - - + + + + - - - - + + + - - - - - - - - - - + + + + + + + - - - - - - + + - - - - - + + - - Braille-example-2025-10-29-P1.png - - P2 -> Braille-example-2025-10-29-P2.png - - P3 -> Braille-example-2025-10-29-P3.png - - P4 -> Braille-example-2025-10-29-P4.png - - P5 -> Braille-example-2025-10-29-P5.png - - P6 -> Braille-example-2025-10-29-P6.png - - P7 -> Braille-example-2025-10-29-P7.png - - P8 -> Braille-example-2025-10-29-P8.png -MD: Braille-example-2025-10-29.md HTML: Braille-example-2025-10-29.html + + Braille-example-2025-12-17-P1.png + - P2 -> Braille-example-2025-12-17-P2.png + - P3 -> Braille-example-2025-12-17-P3.png + - P4 -> Braille-example-2025-12-17-P4.png + - P5 -> Braille-example-2025-12-17-P5.png + - P6 -> Braille-example-2025-12-17-P6.png + - P7 -> Braille-example-2025-12-17-P7.png + - P8 -> Braille-example-2025-12-17-P8.png +MD: Braille-example-2025-12-17.md HTML: Braille-example-2025-12-17.html ]]> \ No newline at end of file diff --git a/target/surefire-reports/com.studentgui.apphelpers.DatabaseContactLogTest.txt b/target/surefire-reports/com.studentgui.apphelpers.DatabaseContactLogTest.txt index 2f7fa3b..3179eb4 100644 --- a/target/surefire-reports/com.studentgui.apphelpers.DatabaseContactLogTest.txt +++ b/target/surefire-reports/com.studentgui.apphelpers.DatabaseContactLogTest.txt @@ -1,4 +1,4 @@ ------------------------------------------------------------------------------- Test set: com.studentgui.apphelpers.DatabaseContactLogTest ------------------------------------------------------------------------------- -Tests run: 1, Failures: 0, Errors: 0, Skipped: 0, Time elapsed: 0.329 s - in com.studentgui.apphelpers.DatabaseContactLogTest +Tests run: 1, Failures: 0, Errors: 0, Skipped: 0, Time elapsed: 0.286 s - in com.studentgui.apphelpers.DatabaseContactLogTest diff --git a/target/surefire-reports/com.studentgui.apphelpers.SessionJsonWriterTest.txt b/target/surefire-reports/com.studentgui.apphelpers.SessionJsonWriterTest.txt index 74c06ce..ba3ed6d 100644 --- a/target/surefire-reports/com.studentgui.apphelpers.SessionJsonWriterTest.txt +++ b/target/surefire-reports/com.studentgui.apphelpers.SessionJsonWriterTest.txt @@ -1,4 +1,4 @@ ------------------------------------------------------------------------------- Test set: com.studentgui.apphelpers.SessionJsonWriterTest ------------------------------------------------------------------------------- -Tests run: 1, Failures: 0, Errors: 0, Skipped: 0, Time elapsed: 0.254 s - in com.studentgui.apphelpers.SessionJsonWriterTest +Tests run: 1, Failures: 0, Errors: 0, Skipped: 0, Time elapsed: 0.115 s - in com.studentgui.apphelpers.SessionJsonWriterTest diff --git a/target/surefire-reports/com.studentgui.apphelpers.SqlGenerateTest.txt b/target/surefire-reports/com.studentgui.apphelpers.SqlGenerateTest.txt index edc7e35..448ffcf 100644 --- a/target/surefire-reports/com.studentgui.apphelpers.SqlGenerateTest.txt +++ b/target/surefire-reports/com.studentgui.apphelpers.SqlGenerateTest.txt @@ -1,4 +1,4 @@ ------------------------------------------------------------------------------- Test set: com.studentgui.apphelpers.SqlGenerateTest ------------------------------------------------------------------------------- -Tests run: 1, Failures: 0, Errors: 0, Skipped: 0, Time elapsed: 0 s - in com.studentgui.apphelpers.SqlGenerateTest +Tests run: 1, Failures: 0, Errors: 0, Skipped: 0, Time elapsed: 0.003 s - in com.studentgui.apphelpers.SqlGenerateTest diff --git a/target/surefire-reports/com.studentgui.apppages.JLineGraphDeterministicJitterTest.txt b/target/surefire-reports/com.studentgui.apppages.JLineGraphDeterministicJitterTest.txt index a4c69fb..dd23361 100644 --- a/target/surefire-reports/com.studentgui.apppages.JLineGraphDeterministicJitterTest.txt +++ b/target/surefire-reports/com.studentgui.apppages.JLineGraphDeterministicJitterTest.txt @@ -1,4 +1,4 @@ ------------------------------------------------------------------------------- Test set: com.studentgui.apppages.JLineGraphDeterministicJitterTest ------------------------------------------------------------------------------- -Tests run: 1, Failures: 0, Errors: 0, Skipped: 0, Time elapsed: 0.164 s - in com.studentgui.apppages.JLineGraphDeterministicJitterTest +Tests run: 1, Failures: 0, Errors: 0, Skipped: 0, Time elapsed: 0.106 s - in com.studentgui.apppages.JLineGraphDeterministicJitterTest diff --git a/target/surefire-reports/com.studentgui.test.BrailleDatabaseTest.txt b/target/surefire-reports/com.studentgui.test.BrailleDatabaseTest.txt index 96c373f..482dbe9 100644 --- a/target/surefire-reports/com.studentgui.test.BrailleDatabaseTest.txt +++ b/target/surefire-reports/com.studentgui.test.BrailleDatabaseTest.txt @@ -1,4 +1,4 @@ ------------------------------------------------------------------------------- Test set: com.studentgui.test.BrailleDatabaseTest ------------------------------------------------------------------------------- -Tests run: 1, Failures: 0, Errors: 0, Skipped: 0, Time elapsed: 0.129 s - in com.studentgui.test.BrailleDatabaseTest +Tests run: 1, Failures: 0, Errors: 0, Skipped: 0, Time elapsed: 0.134 s - in com.studentgui.test.BrailleDatabaseTest diff --git a/target/surefire-reports/com.studentgui.test.BrailleSmokeTest.txt b/target/surefire-reports/com.studentgui.test.BrailleSmokeTest.txt index 84250f7..773e42a 100644 --- a/target/surefire-reports/com.studentgui.test.BrailleSmokeTest.txt +++ b/target/surefire-reports/com.studentgui.test.BrailleSmokeTest.txt @@ -1,4 +1,4 @@ ------------------------------------------------------------------------------- Test set: com.studentgui.test.BrailleSmokeTest ------------------------------------------------------------------------------- -Tests run: 1, Failures: 0, Errors: 0, Skipped: 0, Time elapsed: 0.416 s - in com.studentgui.test.BrailleSmokeTest +Tests run: 1, Failures: 0, Errors: 0, Skipped: 0, Time elapsed: 0.608 s - in com.studentgui.test.BrailleSmokeTest diff --git a/target/surefire-reports/com.studentgui.test.DatabaseEdgeCasesTest.txt b/target/surefire-reports/com.studentgui.test.DatabaseEdgeCasesTest.txt index 468891c..77567d0 100644 --- a/target/surefire-reports/com.studentgui.test.DatabaseEdgeCasesTest.txt +++ b/target/surefire-reports/com.studentgui.test.DatabaseEdgeCasesTest.txt @@ -1,4 +1,4 @@ ------------------------------------------------------------------------------- Test set: com.studentgui.test.DatabaseEdgeCasesTest ------------------------------------------------------------------------------- -Tests run: 3, Failures: 0, Errors: 0, Skipped: 0, Time elapsed: 0.129 s - in com.studentgui.test.DatabaseEdgeCasesTest +Tests run: 3, Failures: 0, Errors: 0, Skipped: 0, Time elapsed: 0.124 s - in com.studentgui.test.DatabaseEdgeCasesTest diff --git a/target/surefire-reports/com.studentgui.test.DatabaseTest.txt b/target/surefire-reports/com.studentgui.test.DatabaseTest.txt index 945d1e2..a875a8f 100644 --- a/target/surefire-reports/com.studentgui.test.DatabaseTest.txt +++ b/target/surefire-reports/com.studentgui.test.DatabaseTest.txt @@ -1,4 +1,4 @@ ------------------------------------------------------------------------------- Test set: com.studentgui.test.DatabaseTest ------------------------------------------------------------------------------- -Tests run: 1, Failures: 0, Errors: 0, Skipped: 0, Time elapsed: 0.079 s - in com.studentgui.test.DatabaseTest +Tests run: 1, Failures: 0, Errors: 0, Skipped: 0, Time elapsed: 0.088 s - in com.studentgui.test.DatabaseTest diff --git a/target/surefire-reports/com.studentgui.test.ExportBrailleReportsTest.txt b/target/surefire-reports/com.studentgui.test.ExportBrailleReportsTest.txt index 3b5bd78..2b4c4bf 100644 --- a/target/surefire-reports/com.studentgui.test.ExportBrailleReportsTest.txt +++ b/target/surefire-reports/com.studentgui.test.ExportBrailleReportsTest.txt @@ -1,4 +1,4 @@ ------------------------------------------------------------------------------- Test set: com.studentgui.test.ExportBrailleReportsTest ------------------------------------------------------------------------------- -Tests run: 1, Failures: 0, Errors: 0, Skipped: 0, Time elapsed: 1.013 s - in com.studentgui.test.ExportBrailleReportsTest +Tests run: 1, Failures: 0, Errors: 0, Skipped: 0, Time elapsed: 1.068 s - in com.studentgui.test.ExportBrailleReportsTest