001package edu.pdx.cs410J.grader.gradebook.ui;
002
003import edu.pdx.cs410J.ParserException;
004import edu.pdx.cs410J.grader.gradebook.*;
005
006import javax.swing.*;
007import javax.swing.border.Border;
008import java.awt.*;
009import java.awt.event.*;
010import java.io.File;
011import java.io.IOException;
012import java.io.PrintStream;
013import java.net.URL;
014import java.util.ArrayList;
015import java.util.TimerTask;
016import java.util.prefs.Preferences;
017
018/**
019 * This class is a main GUI for manipulate the grade book for CS410J.
020 *
021 * @author David Whitlock
022 * @version $Revision: 1.15 $
023 * @since Fall 2000
024 */
025@SuppressWarnings("serial")
026public class GradeBookGUI extends JFrame {
027
028  /** The prefix for the "recent file" preference */
029  private static final String RECENT_FILE = "GradeBook_recent_file_";
030
031  /** The maximum number of recent files */
032  private static final int MAX_RECENT_FILES = 4;
033
034  ///////////////////////  Instance Fields  ///////////////////////
035
036  private GradeBook book;
037  private GradeBookPanel bookPanel;
038  private File file;       // Where the grade book lives
039
040  /** The most recently visited files (by name) */
041  private ArrayList<String> recentFiles;
042
043  /** Preferences for using this application */
044  private Preferences prefs;
045
046  /** A menu listing the recently visited gradebook files */
047  private JMenu recentFilesMenu;
048
049  /** A label that displays status messages */
050  private JLabel status;
051
052  /** An action for creating a new grade book file */
053  private Action newAction;
054
055  /** An action for importing students into a gradebook */
056  private Action importAction;
057
058  /** An action for opening a gradebook */
059  private Action openAction;
060
061  /** An action for saving a gradebook */
062  private Action saveAction;
063
064  /** An action for saving a gradebook with a given file name */
065  private Action saveAsAction;
066
067  /** An action for exiting the GUI */
068  private Action exitAction;
069
070  ///////////////////////  Constructors  ///////////////////////
071
072  /**
073   * Create and lay out a new <code>GradeBookGUI</code>
074   */
075  public GradeBookGUI(String title) {
076    super(title);
077
078    // Read preference information
079    this.prefs = Preferences.userNodeForPackage(this.getClass());
080    this.recentFiles = new ArrayList<String>();
081    for (int i = 0; i < MAX_RECENT_FILES; i++) {
082      String recent = this.prefs.get(RECENT_FILE + i, null);
083      if (recent != null) {
084        File file = new File(recent);
085        if (file.exists() && file.isFile()) {
086          this.recentFiles.add(0, recent);
087        }
088      }
089    }
090    
091    // Set up the actions
092    this.setupActions();
093
094    // Add the menus
095    JMenuBar menuBar = new JMenuBar();
096    this.setJMenuBar(menuBar);
097    this.addFileMenu(menuBar);
098    this.updateRecentFilesMenu();
099
100    this.getContentPane().setLayout(new BorderLayout());
101
102    // Add the tool bar
103    this.getContentPane().add(this.createToolBar(),
104                              BorderLayout.NORTH);
105
106    // Add the GradeBookPanel
107    this.bookPanel = new GradeBookPanel(this);
108    this.getContentPane().add(this.bookPanel, BorderLayout.CENTER);
109
110    // Add the status label.  Status messages are displaed for five
111    // seconds
112    this.status = new JLabel(" ") {
113        private java.util.Timer timer =
114          new java.util.Timer(true /* isDaemon */);
115        private TimerTask lastTask = null;
116
117        public void setText(String text) {
118          super.setText(text);
119          if ("".equals(text.trim())) {
120            // Avoid recusion
121            return;
122          }
123
124          if (lastTask != null) {
125            lastTask.cancel();
126          }
127          lastTask = new TimerTask() {
128              public void run() {
129                SwingUtilities.invokeLater(new Runnable() {
130                    public void run() {
131                      // Empty label looks funny with border
132                      GradeBookGUI.this.status.setText(" ");
133                    }
134                  });
135              }
136            };
137          timer.schedule(lastTask, 5 * 1000);
138        }
139      };
140    Border inside = BorderFactory.createEmptyBorder(0, 2, 0, 0);
141    Border outside = BorderFactory.createLoweredBevelBorder();
142    Border border =
143      BorderFactory.createCompoundBorder(outside, inside); 
144    this.status.setBorder(border);
145    this.getContentPane().add(this.status, BorderLayout.SOUTH);
146
147    // Handle exit events
148    this.addWindowListener(new WindowAdapter() {
149        public void windowClosing(WindowEvent e) {
150          exit();
151        }
152      });
153  }
154
155  /**
156   * Initializes the action objects that encapsulate actions that can
157   * be performed by various widgets in this GUI.
158   */
159  private void setupActions() {
160    ClassLoader cl = Thread.currentThread().getContextClassLoader();
161
162    // New action
163    this.newAction = new AbstractAction("New ...") {
164        public void actionPerformed(ActionEvent e) {
165          newGradeBook();
166        }
167      };
168    this.newAction.putValue(Action.SHORT_DESCRIPTION,
169                               "New grade book");
170    this.newAction.putValue(Action.LONG_DESCRIPTION,
171                               "Create a new grade book");
172    this.newAction.putValue(Action.ACCELERATOR_KEY, 
173                               KeyStroke.getKeyStroke("control N"));
174    this.newAction.putValue(Action.MNEMONIC_KEY, 
175                               new Integer(KeyEvent.VK_N));
176    URL newURL =
177      cl.getResource("toolbarButtonGraphics/general/New16.gif");
178    if (newURL != null) {
179      this.newAction.putValue(Action.SMALL_ICON, new ImageIcon(newURL));
180    }
181
182    // Open action
183    this.openAction = new AbstractAction("Open ...") {
184        public void actionPerformed(ActionEvent e) {
185          open();
186        }
187      };
188    this.openAction.putValue(Action.SHORT_DESCRIPTION,
189                               "Open grade book");
190    this.openAction.putValue(Action.LONG_DESCRIPTION,
191                               "Open a grade book file");
192    this.openAction.putValue(Action.ACCELERATOR_KEY, 
193                               KeyStroke.getKeyStroke("control O"));
194    this.openAction.putValue(Action.MNEMONIC_KEY, 
195                               new Integer(KeyEvent.VK_O));
196    URL openURL =
197      cl.getResource("toolbarButtonGraphics/general/Open16.gif");
198    if (openURL != null) {
199      this.openAction.putValue(Action.SMALL_ICON, new ImageIcon(openURL));
200    }
201
202    // Import action
203    this.importAction = new AbstractAction("Import...") {
204        public void actionPerformed(ActionEvent e) {
205          importStudent();
206        }
207      };
208    this.importAction.putValue(Action.SHORT_DESCRIPTION,
209                               "Import students");
210    this.importAction.putValue(Action.LONG_DESCRIPTION,
211                               "Import students into a grade book");
212    this.importAction.putValue(Action.ACCELERATOR_KEY, 
213                               KeyStroke.getKeyStroke("control I"));
214    this.importAction.putValue(Action.MNEMONIC_KEY, 
215                               new Integer(KeyEvent.VK_I));
216    this.importAction.setEnabled(false);
217    URL importURL =
218      cl.getResource("toolbarButtonGraphics/general/Import16.gif");
219    if (importURL != null) {
220      this.importAction.putValue(Action.SMALL_ICON, new ImageIcon(importURL));
221    }
222
223    // Save action
224    this.saveAction = new AbstractAction("Save") {
225        public void actionPerformed(ActionEvent e) {
226          save();
227        }
228      };
229    this.saveAction.putValue(Action.SHORT_DESCRIPTION,
230                               "Save grade book");
231    this.saveAction.putValue(Action.LONG_DESCRIPTION,
232                               "Save the current grade book file");
233    this.saveAction.putValue(Action.ACCELERATOR_KEY, 
234                               KeyStroke.getKeyStroke("control S"));
235    this.saveAction.putValue(Action.MNEMONIC_KEY, 
236                               new Integer(KeyEvent.VK_S));
237    this.saveAction.setEnabled(false);
238    URL saveURL =
239      cl.getResource("toolbarButtonGraphics/general/Save16.gif");
240    if (saveURL != null) {
241      this.saveAction.putValue(Action.SMALL_ICON, new ImageIcon(saveURL));
242    }
243
244    // Save As action
245    this.saveAsAction = new AbstractAction("Save As ...") {
246        public void actionPerformed(ActionEvent e) {
247          saveAs();
248        }
249      };
250    this.saveAsAction.putValue(Action.SHORT_DESCRIPTION,
251                               "Save grade book");
252    this.saveAsAction.putValue(Action.MNEMONIC_KEY, 
253                               new Integer(KeyEvent.VK_A));
254    this.saveAsAction.setEnabled(false);
255    URL saveAsURL =
256      cl.getResource("toolbarButtonGraphics/general/SaveAs16.gif");
257    if (saveAsURL != null) {
258      this.saveAsAction.putValue(Action.SMALL_ICON, new ImageIcon(saveAsURL));
259    }
260
261    // Exit Action
262    this.exitAction = new AbstractAction("Exit") {
263        public void actionPerformed(ActionEvent e) {
264          exit();
265        }
266      };
267    this.exitAction.putValue(Action.SHORT_DESCRIPTION, "Exit");
268    this.exitAction.putValue(Action.LONG_DESCRIPTION,
269                             "Exit the grade book program");
270    this.exitAction.putValue(Action.ACCELERATOR_KEY, 
271                             KeyStroke.getKeyStroke("control Q"));
272    this.exitAction.putValue(Action.MNEMONIC_KEY, 
273                             new Integer(KeyEvent.VK_X));
274  }
275
276  /**
277   * Returns a <code>JToolBar</code> for this GUI
278   */
279  private JToolBar createToolBar() {
280    JToolBar tools = new JToolBar(JToolBar.HORIZONTAL);
281    tools.setFloatable(false);
282    tools.add(this.newAction);
283    tools.add(this.openAction);
284    tools.add(this.importAction);
285    tools.addSeparator();
286    tools.add(this.saveAction);
287    tools.add(this.saveAsAction);
288    return tools;
289  }
290
291  /**
292   * Adds a "File" menu to this gui
293   */
294  private void addFileMenu(JMenuBar menuBar) {
295    JMenu fileMenu = new JMenu("File");
296    fileMenu.setMnemonic(KeyEvent.VK_F);
297    menuBar.add(fileMenu);
298
299    fileMenu.add(new JMenuItem(this.newAction));
300    fileMenu.add(new JMenuItem(this.openAction));
301    fileMenu.add(new JMenuItem(this.importAction));
302
303    fileMenu.addSeparator();
304
305    fileMenu.add(new JMenuItem(this.saveAction));
306    fileMenu.add(new JMenuItem(this.saveAsAction));
307
308    fileMenu.addSeparator();
309
310    this.recentFilesMenu = new JMenu("Recent Files");
311    this.recentFilesMenu.setMnemonic(KeyEvent.VK_R);
312    fileMenu.add(this.recentFilesMenu);
313
314    fileMenu.addSeparator();
315
316    fileMenu.add(new JMenuItem(this.exitAction));
317  }
318
319  /**
320   * Displays a dialog box alerting the user that an error has
321   * occurred.
322   */
323  private void error(String message) {
324    JOptionPane.showMessageDialog(this, message,
325                                  "An error occurred",
326                                  JOptionPane.ERROR_MESSAGE);
327  }
328
329  /**
330   * Displays a dialog box alerting the user that an exception was
331   * thrown
332   */
333  private void error(String message, Exception ex) {
334    JOptionPane.showMessageDialog(this, new String[] {
335      message, ex.getMessage()},
336                                  "An exception was thrown",
337                                  JOptionPane.ERROR_MESSAGE);
338  }
339
340  /**
341   * Makes note that the current <code>file</code> has been changed.
342   * Updates the preferences accordingly.
343   *
344   * @since Fall 2004
345   */
346  private void noteRecentFile() {
347    assert this.file != null;
348    String name = this.file.getAbsolutePath();
349    this.recentFiles.remove(name);
350    this.recentFiles.add(0, name);
351    while (this.recentFiles.size() > MAX_RECENT_FILES) {
352      this.recentFiles.remove(this.recentFiles.size() - 1);
353    }
354    for (int i = 0;
355         i < this.recentFiles.size() && i < MAX_RECENT_FILES;
356         i++) {
357      this.prefs.put(RECENT_FILE + i,
358                     (String) this.recentFiles.get(i));
359    }
360    updateRecentFilesMenu();
361  }
362
363  /**
364   * Updates the contents of the "recent files" menu
365   *
366   * @since Fall 2004
367   */
368  private void updateRecentFilesMenu() {
369    this.recentFilesMenu.removeAll();
370    JMenuItem clear = new JMenuItem("Clear recent files");
371    clear.addActionListener(new ActionListener() {
372        public void actionPerformed(ActionEvent e) {
373          GradeBookGUI.this.recentFiles.clear();
374          updateRecentFilesMenu();
375        }
376      });
377    this.recentFilesMenu.add(clear);
378    this.recentFilesMenu.addSeparator();
379
380    for (int i = 0; i < this.recentFiles.size(); i++) {
381      String fileName = (String) this.recentFiles.get(i);
382      final File file = new File(fileName);
383      if (file.exists()) {
384        JMenuItem item = new JMenuItem((i+1) + ": " + fileName);
385        item.addActionListener(new ActionListener() {
386            public void actionPerformed(ActionEvent e) {
387              open(file);
388            }
389          });
390        this.recentFilesMenu.add(item);
391      }
392    }
393  }
394
395  /**
396   * Saves the grade book being edited to a file
397   */
398  private void save() {
399    if (this.book == null) {
400      // Nothing to do
401      return;
402    }
403
404    if (this.file == null) {
405      saveAs();
406      // saveAs() will recursively invoke save() if the user selects a
407      // file
408      return;
409    }
410
411    // Dump grade to file as XML
412    try {
413      XmlDumper dumper = new XmlDumper(this.file);
414      dumper.dump(this.book);
415      this.status.setText("Saved " + this.file);
416
417    } catch (IOException ex) {
418      error("While writing grade book", ex);
419      return;
420    }
421  }
422
423  /**
424   * Creates a <code>JFileChooser</code> suitable for dealing with
425   * xml files.
426   */
427  private JFileChooser getFileChooser() {
428    JFileChooser chooser = new JFileChooser();
429    if (this.file != null) {
430      chooser.setCurrentDirectory(file.getAbsoluteFile().getParentFile());
431    } else {
432      String cwd = System.getProperty("user.dir");
433      chooser.setCurrentDirectory(new File(cwd));
434    }
435
436    chooser.setFileFilter(new javax.swing.filechooser.FileFilter() {
437        public boolean accept(File file) {
438          if (file.isDirectory()) {
439            return true;
440          }
441
442          String fileName = file.getName();
443          return fileName.endsWith(".xml");
444        }
445
446        public String getDescription() {
447          return "XML files";
448        }
449      });
450    chooser.setFileSelectionMode(JFileChooser.FILES_ONLY);
451    chooser.setMultiSelectionEnabled(false);
452
453    return chooser;
454  }
455
456  /**
457   * Prompts the user for a file to save the grade book in.
458   */
459  private void saveAs() {
460    JFileChooser chooser = this.getFileChooser();
461    chooser.setDialogTitle("Save as...");
462    chooser.setDialogType(JFileChooser.SAVE_DIALOG);
463    int response = chooser.showSaveDialog(this);
464
465    if (response == JFileChooser.APPROVE_OPTION) {
466      this.file = chooser.getSelectedFile();
467      save();
468      noteRecentFile();
469    }
470  }
471
472  /**
473   * Prompts the user for a file to display
474   */
475  private void open() {
476    JFileChooser chooser = this.getFileChooser();
477    chooser.setDialogTitle("Open grade book file");
478    chooser.setDialogType(JFileChooser.OPEN_DIALOG);
479    int response = chooser.showOpenDialog(this);
480
481    if (response == JFileChooser.APPROVE_OPTION) {
482      // Read the grade book from the file and display it in the GUI
483      File file = chooser.getSelectedFile();
484      open(file);
485    }
486  }
487
488  /**
489   * Opens the grade book stored in the given file
490   */ 
491  private void open(File file) {
492    this.checkDirtyGradeBook();
493
494    try {
495      XmlGradeBookParser parser = new XmlGradeBookParser(file);
496      GradeBook book = parser.parse();
497      this.displayGradeBook(book);
498      this.file = file;
499      noteRecentFile();
500
501    } catch (IOException ex) {
502      error("While parsing file " + file.getName(), ex);
503      return;
504
505    } catch (ParserException ex) {
506      error("While parsing file " + file.getName(), ex);
507    }
508
509    this.status.setText("Opened " + file);
510  }
511
512  /**
513   * Prompts the user for a file from which a student is read and
514   * added to the grade book
515   */
516  private void importStudent() {
517    if (this.book == null) {
518      error("You must open a grade book before importing a student");
519      return;
520    }
521    
522    JFileChooser chooser = this.getFileChooser();
523    chooser.setDialogTitle("Import student XML file");
524    chooser.setDialogType(JFileChooser.OPEN_DIALOG);
525    chooser.setFileSelectionMode(JFileChooser.FILES_ONLY);
526    chooser.setMultiSelectionEnabled(true);
527    int response = chooser.showOpenDialog(this);
528    
529    if (response == JFileChooser.APPROVE_OPTION) {
530      // Read the student from each of the selected files and add it
531      // to the grade book
532      File[] files = chooser.getSelectedFiles();
533      for (int i = 0; i < files.length; i++) {
534        File file = files[i];
535        try {
536          XmlStudentParser sp = new XmlStudentParser(file);
537          Student student = sp.parseStudent();
538          this.book.addStudent(student);
539          this.status.setText("Imported " + student.getFullName());
540        
541        } catch (IOException ex) {
542          error("Could not access " + file.getName(), ex);
543          return;
544
545        } catch (ParserException ex) {
546          error("While parsing file " + file.getName(), ex);
547          return;
548        }
549      }
550    }
551  }
552
553  /**
554   * Creates a new grade book to be edited
555   */
556  private void newGradeBook() {
557    // Prompt user for name of grade book
558    String className = 
559      JOptionPane.showInputDialog(this, 
560                                  "Enter the name of the class",
561                                  "Enter class name",
562                                  JOptionPane.INFORMATION_MESSAGE);
563
564    if (className == null || className.equals("")) {
565      return;
566    }
567
568    this.displayGradeBook(new GradeBook(className));
569    this.file = null;
570    this.status.setText("Created new graded book for \"" + className +
571                        "\"");
572  }
573
574  /**
575   * Prompts the user to save modified data and exits
576   */
577  private void exit() {
578    checkDirtyGradeBook();
579    System.exit(0);
580  }
581
582  /**
583   * If the grade book has been modified and not saved, ask the user
584   * if he wants to save it.
585   */
586  private void checkDirtyGradeBook() {
587    if (this.book != null && this.book.isDirty()) {
588      int response = JOptionPane.showConfirmDialog(this, new String[] {
589        "You have made changes to the grade book.",
590        "Do you want to save them?"},
591        "Save Changes?",
592        JOptionPane.YES_NO_CANCEL_OPTION,
593        JOptionPane.QUESTION_MESSAGE);
594      
595      if (response == JOptionPane.YES_OPTION) {
596        save();
597
598      } else if (response == JOptionPane.CANCEL_OPTION) {
599        // Don't exit
600        return;
601      }
602    }
603  }
604
605  /**
606   * Displays a <code>GradeBook</code> in this GUI
607   */
608  private void displayGradeBook(GradeBook book) {
609    this.book = book;
610    this.importAction.setEnabled(true);
611    this.saveAction.setEnabled(true);
612    this.saveAsAction.setEnabled(true);
613    this.setTitle(book.getClassName());
614    this.bookPanel.displayGradeBook(this.book);
615  }
616
617  /**
618   * Main program.  Create and show a <code>GradeBookGUI</code>
619   */
620  public static void main(String[] args) {
621    GradeBookGUI gui = new GradeBookGUI("CS410J Grade Book Program");
622
623    if (args.length > 0) {
624      PrintStream err = System.err;
625      File file = new File(args[0]);
626      if (!file.exists()) {
627        err.println("** " + file + " does not exist");
628
629      } else if (!file.isFile()) {
630        err.println("** " + file + " is not a file");
631
632      } else {
633        gui.open(file);
634      }
635    }
636
637    gui.pack();
638    gui.setVisible(true);
639  }
640
641}