001package com.studentgui.app; 002 003import java.awt.BorderLayout; 004import java.awt.CardLayout; 005import java.awt.FlowLayout; 006import java.awt.Font; 007import java.time.LocalDate; 008import java.time.format.DateTimeParseException; 009import java.util.List; 010 011import javax.swing.JButton; 012import javax.swing.JComboBox; 013import javax.swing.JComponent; 014import javax.swing.JFrame; 015import javax.swing.JLabel; 016import javax.swing.JPanel; 017import javax.swing.JTextField; 018import javax.swing.SwingUtilities; 019import javax.swing.UIManager; 020 021import org.slf4j.Logger; 022import org.slf4j.LoggerFactory; 023 024import com.studentgui.apphelpers.Helpers; 025import com.studentgui.apphelpers.SqlGenerate; 026import com.studentgui.apppages.Abacus; 027import com.studentgui.apppages.Braille; 028import com.studentgui.apppages.BrailleNote; 029import com.studentgui.apppages.BrailleSense; 030import com.studentgui.apppages.CVI; 031import com.studentgui.apppages.ContactLog; 032import com.studentgui.apppages.DigitalLiteracy; 033import com.studentgui.apppages.Homepage; 034import com.studentgui.apppages.IOS; 035import com.studentgui.apppages.InstructionalMaterials; 036import com.studentgui.apppages.JLineGraph; 037import com.studentgui.apppages.Keyboarding; 038import com.studentgui.apppages.Observations; 039import com.studentgui.apppages.ScreenReader; 040import com.studentgui.apppages.SessionNotes; 041import com.studentgui.apptheming.Theme; 042 043/** 044 * Main application entry and UI wiring for the Student Skills Progressions app. 045 * 046 * This class builds the top-level window, menu, and registers the skill pages 047 * (each page is a JPanel). It's intentionally lightweight; most functionality 048 * for database access and page logic lives in helper classes and the page 049 * components under com.studentgui.apppages. 050 */ 051/** 052 * Application bootstrap and top-level UI wiring. Builds the main JFrame, 053 * registers pages, and provides a small top control bar for switching 054 * students and pages. 055 */ 056/** 057 * Application bootstrap and top-level UI wiring. Builds the main JFrame, 058 * registers pages, and provides a small top control bar for switching 059 * students and pages. 060 */ 061/** 062 * Application entry point and top-level UI wiring for the Student Skills 063 * Progressions application. Builds the main frame, menu and registers per-page 064 * panels under a CardLayout. 065 */ 066public class Main { 067 /** 068 * Bootstrap logging/system properties very early so Logback can resolve 069 * file locations and the per-run timestamp before any logger is 070 * initialized. This static block sets APP_HOME and LOG_TS and performs 071 * a cleanup of old log files older than 7 days. 072 */ 073 static { 074 try { 075 // Ensure Helpers.APP_HOME is initialized and use it for logging 076 String appHome = com.studentgui.apphelpers.Helpers.APP_HOME.toString(); 077 System.setProperty("APP_HOME", appHome); 078 // unix epoch seconds appended to per-run log filename 079 String ts = String.valueOf(java.time.Instant.now().getEpochSecond()); 080 System.setProperty("LOG_TS", ts); 081 082 // create logs dir 083 java.nio.file.Path logs = java.nio.file.Paths.get(appHome).resolve("logs"); 084 java.nio.file.Files.createDirectories(logs); 085 086 // Cleanup: remove log files older than 7 days (by last modified time) 087 long cutoff = java.time.Instant.now().minus(java.time.Duration.ofDays(7)).toEpochMilli(); 088 try (java.nio.file.DirectoryStream<java.nio.file.Path> ds = java.nio.file.Files.newDirectoryStream(logs, "log_*.log")) { 089 for (java.nio.file.Path p : ds) { 090 try { 091 java.nio.file.attribute.FileTime ft = java.nio.file.Files.getLastModifiedTime(p); 092 if (ft.toMillis() < cutoff) { 093 java.nio.file.Files.deleteIfExists(p); 094 } 095 } catch (Exception ex) { 096 // Swallow cleanup exceptions; logging isn't available yet. 097 } 098 } 099 } catch (Exception ex) { 100 // ignore 101 } 102 // also remove consolidated data dump files older than retention 103 try (java.nio.file.DirectoryStream<java.nio.file.Path> ds2 = java.nio.file.Files.newDirectoryStream(logs, "data_dumps_*.log")) { 104 for (java.nio.file.Path p : ds2) { 105 try { 106 java.nio.file.attribute.FileTime ft = java.nio.file.Files.getLastModifiedTime(p); 107 if (ft.toMillis() < cutoff) { 108 java.nio.file.Files.deleteIfExists(p); 109 } 110 } catch (Exception ex) { 111 // Swallow cleanup exceptions; logging isn't available yet. 112 } 113 } 114 } catch (Exception ex) { 115 // ignore 116 } 117 } catch (Exception ex) { 118 // If anything here fails, continue — logging may not be configured yet. 119 } 120 } 121 122 private static final Logger LOG = LoggerFactory.getLogger(Main.class); 123 private static JFrame frame; 124 private static JPanel contentPanel; 125 private static JLineGraph sharedGraph; 126 /** 127 * Shared JLineGraph instance used across pages. 128 * 129 * Pages are constructed with the shared graph passed into their 130 * constructors (see recreatePages). The shared graph is registered 131 * with {@link #addSettingsChangeListener(SettingsChangeListener)} so 132 * it receives runtime preference updates. If a page creates its own 133 * page-local JLineGraph instance it should register it with 134 * {@link #addSettingsChangeListener(SettingsChangeListener)} and remove 135 * it when disposed to ensure it receives preference changes and to 136 * avoid leaking listeners. 137 */ 138 // current date used by the top bar (can be updated without recreating pages) 139 private static java.time.LocalDate currentDate; 140 private static String currentStudent; 141 // Listeners to notify when the top-bar date changes 142 private static final java.util.List<DateChangeListener> dateListeners = new java.util.concurrent.CopyOnWriteArrayList<>(); 143 144 /** 145 * Register a listener to be notified when the application date is changed via the top bar. 146 * 147 * @param l listener to register (ignored when null) 148 */ 149 public static void addDateChangeListener(final DateChangeListener l) { 150 if (l != null) { 151 dateListeners.add(l); 152 } 153 } 154 155 /** 156 * Remove a previously registered date change listener. 157 * 158 * @param l listener to remove (ignored when null) 159 */ 160 public static void removeDateChangeListener(final DateChangeListener l) { 161 if (l != null) { 162 dateListeners.remove(l); 163 } 164 } 165 166 /** 167 * Clear all registered date change listeners. 168 */ 169 public static void clearDateChangeListeners() { 170 dateListeners.clear(); 171 } 172 173 /** 174 * Notify all registered date listeners that the application date has changed. 175 * 176 * @param d new application date 177 */ 178 private static void notifyDateChanged(final java.time.LocalDate d) { 179 for (DateChangeListener l : dateListeners) { 180 try { 181 l.dateChanged(d); 182 } catch (Exception ex) { 183 LOG.warn("DateChangeListener threw: {}", ex.toString()); 184 } 185 } 186 } 187 // Student change listeners 188 private static final java.util.List<StudentChangeListener> studentListeners = new java.util.concurrent.CopyOnWriteArrayList<>(); 189 /** 190 * Register a listener to be notified when the selected student is changed. 191 * 192 * @param l listener to register (ignored when null) 193 */ 194 public static void addStudentChangeListener(final StudentChangeListener l) { 195 if (l != null) { 196 studentListeners.add(l); 197 } 198 } 199 200 /** 201 * Remove a previously registered student change listener. 202 * 203 * @param l listener to remove (ignored when null) 204 */ 205 public static void removeStudentChangeListener(final StudentChangeListener l) { 206 if (l != null) { 207 studentListeners.remove(l); 208 } 209 } 210 211 /** 212 * Clear all registered student change listeners. 213 */ 214 public static void clearStudentChangeListeners() { 215 studentListeners.clear(); 216 } 217 218 /** 219 * Notify registered student change listeners that the selected student has changed. 220 * 221 * @param s new selected student name 222 */ 223 private static void notifyStudentChanged(final String s) { 224 currentStudent = s; 225 for (StudentChangeListener l : studentListeners) { 226 try { 227 l.studentChanged(s); 228 } catch (Exception ex) { 229 LOG.warn("StudentChangeListener threw: {}", ex.toString()); 230 } 231 } 232 } 233 234 235 // Settings change listeners 236 private static final java.util.List<SettingsChangeListener> settingsListeners = new java.util.concurrent.CopyOnWriteArrayList<>(); 237 238 /** 239 * Register a listener to be notified when application settings change. 240 * Implementations should read values from {@link com.studentgui.apphelpers.Settings} 241 * when {@link SettingsChangeListener#settingsChanged()} is invoked. 242 * 243 * @param l listener to register (ignored when null) 244 */ 245 public static void addSettingsChangeListener(final SettingsChangeListener l) { 246 if (l != null) { 247 settingsListeners.add(l); 248 } 249 } 250 251 /** 252 * Remove a previously registered settings change listener. 253 * 254 * @param l listener to remove (ignored when null) 255 */ 256 public static void removeSettingsChangeListener(final SettingsChangeListener l) { 257 if (l != null) { 258 settingsListeners.remove(l); 259 } 260 } 261 262 /** 263 * Clear all registered settings change listeners. 264 */ 265 public static void clearSettingsChangeListeners() { 266 settingsListeners.clear(); 267 } 268 269 /** 270 * Notify all registered settings listeners that application settings have been changed. 271 * This is typically invoked after persisting preferences through 272 * {@link com.studentgui.apphelpers.Settings}. 273 */ 274 public static void notifySettingsChanged() { 275 for (SettingsChangeListener l : settingsListeners) { 276 try { 277 l.settingsChanged(); 278 } catch (Exception ex) { 279 LOG.warn("SettingsChangeListener threw: {}", ex.toString()); 280 } 281 } 282 } 283 284 /** 285 * Application entry point. Initializes helpers, database, and launches the 286 * Swing UI on the EDT. 287 * 288 * @param args command-line arguments (unused) 289 */ 290 public static void main(final String[] args) { 291 // Apply saved look and feel (default to light) 292 // Settings.get and setTheme handle any expected failures internally; 293 // call directly so we avoid a broad RuntimeException catch. 294 String saved = com.studentgui.apphelpers.Settings.get("theme", "light"); 295 setTheme(saved); 296 297 // Initialize helpers and DB 298 Helpers.setStartDir(); 299 Helpers.createFolderHierarchy(); 300 SqlGenerate.initializeDatabase(); 301 302 SwingUtilities.invokeLater(() -> { 303 frame = new JFrame("Student Skills Progressions"); 304 frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE); 305 frame.setSize(1000, 700); 306 frame.setLocationRelativeTo(null); 307 308 // Menu bar: obtain the app menu bar from Theme, insert a File->Exit menu at the far left 309 javax.swing.JMenuBar themeBar = Theme.createMenuBar(); 310 if (themeBar == null) { 311 themeBar = new javax.swing.JMenuBar(); 312 } 313 javax.swing.JMenu fileMenu = new javax.swing.JMenu("File"); 314 javax.swing.JMenuItem exitItem = new javax.swing.JMenuItem("Exit"); 315 exitItem.addActionListener(e -> { 316 LOG.info("Exit requested via File->Exit"); 317 if (frame != null) { 318 frame.dispose(); 319 } 320 System.exit(0); 321 }); 322 fileMenu.add(exitItem); 323 // Insert file menu at position 0 so it appears on the far left 324 themeBar.add(fileMenu, 0); 325 // Ensure the Themes menu (if present) appears immediately after File 326 int themesIdx = -1; 327 for (int i = 0; i < themeBar.getMenuCount(); i++) { 328 javax.swing.JMenu m = themeBar.getMenu(i); 329 if (m != null && "Themes".equals(m.getText())) { themesIdx = i; break; } 330 } 331 if (themesIdx > 1) { 332 javax.swing.JMenu themesMenu = themeBar.getMenu(themesIdx); 333 themeBar.remove(themesIdx); 334 themeBar.add(themesMenu, 1); 335 } 336 frame.setJMenuBar(themeBar); 337 338 339 contentPanel = new JPanel(new CardLayout()); 340 frame.add(contentPanel, BorderLayout.CENTER); 341 342 // Top control bar: student selector, date, and navigation 343 JPanel topBar = buildTopBar(); 344 frame.add(topBar, BorderLayout.NORTH); 345 346 // Create initial shared graph and pages for the first student 347 sharedGraph = new JLineGraph(); 348 // Register shared graph to receive settings change notifications 349 addSettingsChangeListener(sharedGraph); 350 List<String> students = Helpers.getStudents(); 351 String demoStudent = students.isEmpty() ? "Demo Student" : students.get(0); 352 LocalDate today = LocalDate.now(); 353 currentDate = today; 354 recreatePages(demoStudent, today); 355 356 frame.setVisible(true); 357 }); 358 } 359 360 /** 361 * Change application theme at runtime. Supported values: "light", "dark", "darcula". 362 * This method updates the installed Look and Feel and refreshes the main frame. 363 * 364 * @param theme human-friendly theme name or fully-qualified LookAndFeel class name 365 */ 366 public static void setTheme(final String theme) { 367 try { 368 String t = theme == null ? "light" : theme; 369 // Common keywords for bundled themes 370 switch (t.toLowerCase()) { 371 case "dark": 372 case "flatdarklaf": 373 UIManager.setLookAndFeel(new com.formdev.flatlaf.FlatDarkLaf()); 374 break; 375 case "darcula": 376 // Darcula-like: use FlatDarkLaf as fallback 377 UIManager.setLookAndFeel(new com.formdev.flatlaf.FlatDarkLaf()); 378 break; 379 case "light": 380 case "flatlightlaf": 381 UIManager.setLookAndFeel(new com.formdev.flatlaf.FlatLightLaf()); 382 break; 383 default: 384 // If the string looks like a fully-qualified class name, try to set it directly. 385 if (t.contains(".")) { 386 try { 387 UIManager.setLookAndFeel(t); 388 } catch (ReflectiveOperationException | javax.swing.UnsupportedLookAndFeelException ex) { 389 // Try to instantiate via reflection 390 try { 391 Class<?> c = Class.forName(t); 392 Object o = c.getDeclaredConstructor().newInstance(); 393 if (o instanceof javax.swing.LookAndFeel) { 394 UIManager.setLookAndFeel((javax.swing.LookAndFeel) o); 395 } else { 396 // fallback to light 397 UIManager.setLookAndFeel(new com.formdev.flatlaf.FlatLightLaf()); 398 } 399 } catch (ReflectiveOperationException | javax.swing.UnsupportedLookAndFeelException ex2) { 400 LOG.error("Failed to set look and feel by class name {}", t, ex2); 401 UIManager.setLookAndFeel(new com.formdev.flatlaf.FlatLightLaf()); 402 } 403 } 404 } else { 405 // Try to find an installed LAF by name 406 boolean applied = false; 407 for (UIManager.LookAndFeelInfo info : UIManager.getInstalledLookAndFeels()) { 408 if (info.getName().equalsIgnoreCase(t) || info.getName().toLowerCase().contains(t.toLowerCase())) { 409 UIManager.setLookAndFeel(info.getClassName()); 410 applied = true; 411 break; 412 } 413 } 414 if (!applied) { 415 // default fallback 416 UIManager.setLookAndFeel(new com.formdev.flatlaf.FlatLightLaf()); 417 } 418 } 419 break; 420 } 421 if (frame != null) { 422 javax.swing.SwingUtilities.updateComponentTreeUI(frame); 423 frame.pack(); 424 } 425 } catch (ReflectiveOperationException | javax.swing.UnsupportedLookAndFeelException | IllegalArgumentException e) { 426 LOG.error("Failed to set theme {}", theme, e); 427 } 428 } 429 430 private static JPanel buildTopBar() { 431 JPanel bar = new JPanel(new FlowLayout(FlowLayout.LEFT)); 432 List<String> students = Helpers.getStudents(); 433 JComboBox<String> studentBox = new JComboBox<>(students.toArray(new String[0])); 434 studentBox.setEditable(false); 435 436 JLabel studentLabel = new JLabel("Student:"); 437 studentLabel.setFont(new Font(Font.SANS_SERIF, Font.PLAIN, 24)); 438 JLabel dateLabel = new JLabel("Date (YYYY-MM-DD):"); 439 dateLabel.setFont(new Font(Font.SANS_SERIF, Font.PLAIN, 24)); 440 JTextField dateField = new JTextField(LocalDate.now().toString(), 10); 441 442 JButton goBtn = new JButton("Apply"); 443 444 bar.add(studentLabel); 445 bar.add(studentBox); 446 bar.add(dateLabel); 447 bar.add(dateField); 448 bar.add(goBtn); 449 450 goBtn.addActionListener(e -> { 451 String selected = (String) studentBox.getSelectedItem(); 452 LocalDate date = LocalDate.now(); 453 try { 454 date = LocalDate.parse(dateField.getText()); 455 } catch (DateTimeParseException ex) { 456 // keep today 457 } 458 // Update the app's current date and selected student without recreating pages; show a confirmation dialog. 459 currentDate = date; 460 currentStudent = selected; 461 javax.swing.JOptionPane.showMessageDialog(frame, 462 "The date has been updated to " + date.toString(), 463 "Date Updated", 464 javax.swing.JOptionPane.INFORMATION_MESSAGE); 465 // Notify registered pages so they can update any internal state 466 notifyDateChanged(date); 467 notifyStudentChanged(selected); 468 }); 469 470 // Navigation buttons removed from top bar per UI request; pages can still be selected via menu 471 472 return bar; 473 } 474 475 /** 476 * Recreate per-page panels for the provided student and date. This replaces 477 * the CardLayout content so the shared graph and pages are reset. 478 */ 479 /** 480 * Recreate per-page panels for the provided student and date. This 481 * replaces the CardLayout content so the shared graph and pages are reset. 482 * 483 * @param student selected student's display name 484 * @param date the session date for newly created pages 485 */ 486 private static void recreatePages(final String student, final LocalDate date) { 487 // recreate the pages with a fresh sharedGraph so the graph is reset for the selected student/date 488 if (sharedGraph == null) { 489 sharedGraph = new JLineGraph(); 490 } else { 491 sharedGraph = new JLineGraph(); 492 } 493 494 // Clear any previous listeners to avoid stale references 495 clearDateChangeListeners(); 496 clearStudentChangeListeners(); 497 498 contentPanel.removeAll(); 499 contentPanel.add(Homepage.create(), "homepage"); 500 501 // Instantiate pages into locals so we can register listeners if they implement the interface 502 Braille braille = new Braille(student, date, sharedGraph); 503 contentPanel.add(braille, "braille"); 504 if (braille instanceof DateChangeListener d) { 505 addDateChangeListener(d); 506 } 507 if (braille instanceof StudentChangeListener s) { 508 addStudentChangeListener(s); 509 } 510 511 Abacus abacus = new Abacus(student, date, sharedGraph); 512 contentPanel.add(abacus, "abacus"); 513 if (abacus instanceof DateChangeListener d2) { 514 addDateChangeListener(d2); 515 } 516 if (abacus instanceof StudentChangeListener s2) { 517 addStudentChangeListener(s2); 518 } 519 520 BrailleNote brailleNote = new BrailleNote(student, date, sharedGraph); 521 contentPanel.add(brailleNote, "braillenote"); 522 if (brailleNote instanceof DateChangeListener d3) { 523 addDateChangeListener(d3); 524 } 525 if (brailleNote instanceof StudentChangeListener s3) { 526 addStudentChangeListener(s3); 527 } 528 529 DigitalLiteracy dl = new DigitalLiteracy(student, date, sharedGraph); 530 contentPanel.add(dl, "digitalliteracy"); 531 if (dl instanceof DateChangeListener d4) { 532 addDateChangeListener(d4); 533 } 534 if (dl instanceof StudentChangeListener s4) { 535 addStudentChangeListener(s4); 536 } 537 538 // pages that don't currently need date-driven updates remain created inline 539 contentPanel.add(new BrailleSense(student, date, sharedGraph), "braillesense"); 540 contentPanel.add(new CVI(student, date, sharedGraph), "cvi"); 541 542 IOS ios = new IOS(student, date, sharedGraph); 543 contentPanel.add(ios, "ios"); 544 if (ios instanceof DateChangeListener d5) { 545 addDateChangeListener(d5); 546 } 547 if (ios instanceof StudentChangeListener s5) { 548 addStudentChangeListener(s5); 549 } 550 551 Keyboarding keyboarding = new Keyboarding(student, date, sharedGraph); 552 contentPanel.add(keyboarding, "keyboarding"); 553 if (keyboarding instanceof DateChangeListener d6) { 554 addDateChangeListener(d6); 555 } 556 if (keyboarding instanceof StudentChangeListener s6) { 557 addStudentChangeListener(s6); 558 } 559 560 contentPanel.add(new Observations(student, date), "observations"); 561 562 ScreenReader sr = new ScreenReader(student, date, sharedGraph); 563 contentPanel.add(sr, "screenreader"); 564 if (sr instanceof DateChangeListener d7) { 565 addDateChangeListener(d7); 566 } 567 if (sr instanceof StudentChangeListener s7) { 568 addStudentChangeListener(s7); 569 } 570 571 contentPanel.add(new SessionNotes(student, date, sharedGraph), "sessionnotes"); 572 contentPanel.add(new ContactLog(student, date, sharedGraph), "contactlog"); 573 contentPanel.add(new InstructionalMaterials(), "instructionalmaterials"); 574 575 contentPanel.revalidate(); 576 contentPanel.repaint(); 577 showPage("homepage", null); 578 } 579 580 /** 581 * Show a page previously registered with the CardLayout. If a component 582 * is provided and not yet added it will be registered under the given name. 583 * 584 * @param name registration name for the page 585 * @param comp optional component instance to add (may be null) 586 */ 587 public static void showPage(final String name, final JComponent comp) { 588 CardLayout cl = (CardLayout) contentPanel.getLayout(); 589 if (comp != null && comp.getParent() == null) { 590 contentPanel.add(comp, name); 591 } 592 cl.show(contentPanel, name); 593 } 594 595 /** 596 * Private constructor to prevent instantiation of this utility/entry class. 597 */ 598 private Main() { 599 throw new AssertionError("Not instantiable"); 600 } 601}