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