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, 28));
153        gbc.gridx = 0; gbc.gridy = 0; gbc.gridwidth = GridBagConstraints.REMAINDER;
154        dataEntryPanel.add(this.titleLabel, gbc);
155
156    // compute label width using the shared PhaseScoreField label font so wrapping is stable across themes
157    java.awt.Font labelFont = com.studentgui.uicomp.PhaseScoreField.getLabelFont();
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    /**
394     * Update the displayed date for the ScreenReader page and refresh content.
395     *
396     * Stores `dateParam` and triggers `refreshGraph()` and title update on the
397     * Swing EDT so the UI reflects the new date selection.
398     *
399     * @param newDate the date to display (may be null to use current date)
400     */
401
402    public void dateChanged(LocalDate newDate) {
403        this.dateParam = newDate;
404        SwingUtilities.invokeLater(() -> {
405            refreshGraph();
406            updateTitleDate();
407        });
408    }
409
410    @Override
411    /**
412     * Update the selected student for the ScreenReader page and refresh content.
413     *
414     * Sets `studentNameParam` and schedules a UI refresh on the Swing EDT to
415     * reload data and update the page title.
416     *
417     * @param newStudent student identifier (name or id) to display; may be null
418     */
419
420    public void studentChanged(String newStudent) {
421        this.studentNameParam = newStudent;
422        SwingUtilities.invokeLater(() -> {
423            refreshGraph();
424            updateTitleDate();
425        });
426    }
427
428    private void updateTitleDate() {
429        try {
430            String dateStr = this.dateParam != null ? this.dateParam.toString() : java.time.LocalDate.now().toString();
431            this.titleLabel.setText(baseTitle + " - " + dateStr);
432        } catch (Exception ex) {
433            this.titleLabel.setText(baseTitle);
434        }
435    }
436
437    private void openLatestPlot() {
438        java.nio.file.Path p = com.studentgui.apphelpers.Helpers.latestPlotPath(this.studentNameParam, "ScreenReader");
439        if (p == null) {
440            com.studentgui.apphelpers.UiNotifier.show("No ScreenReader plot found for student");
441            return;
442        }
443    try { java.awt.Desktop.getDesktop().open(p.toFile()); }
444    catch (java.io.IOException | UnsupportedOperationException | SecurityException ex) { com.studentgui.apphelpers.UiNotifier.show("Unable to open plot: " + p.getFileName().toString()); }
445    }
446
447}