Programming Assignment 5

A Simple Text Editor using Java Swing

Example Implementation

/**
 * Implementation of Assignment 5: A Simple Text Editor using Java Swing.
 *
 * @author     Cheng, Jade
 * @assignment CSCI 2912 Assignment 5
 * @date       March 31, 2012
 */


import java.awt.Font;
import java.awt.Toolkit;
import java.awt.event.ActionEvent;
import java.awt.event.ActionListener;
import java.awt.event.InputEvent;
import java.awt.event.KeyEvent;
import java.io.ByteArrayOutputStream;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileWriter;
import java.io.IOException;

import javax.swing.JFileChooser;
import javax.swing.JFrame;
import javax.swing.JMenu;
import javax.swing.JMenuBar;
import javax.swing.JMenuItem;
import javax.swing.JOptionPane;
import javax.swing.JScrollPane;
import javax.swing.JTextArea;
import javax.swing.KeyStroke;
import javax.swing.SwingUtilities;
import javax.swing.UIManager;
import javax.swing.event.DocumentEvent;
import javax.swing.event.DocumentListener;

/**
 * Implementation of Assignment 5: A Simple Text Editor using Java Swing.
 */

public final class ChengJade5 implements Runnable {

  /**
   * The main entry point of the application.
   *
   * @param args The command-line arguments (ignored).
   */

  public static void main(final String[] args) {
    // Start Swing and show the main frame.
    SwingUtilities.invokeLater(new ChengJade5());
  }

  @Override
  public void run() {
    // Use the look-and-feel of the system; ignore errors.
    try {
      UIManager.setLookAndFeel(UIManager.getSystemLookAndFeelClassName());
      System.setProperty("apple.laf.useScreenMenuBar", "true");
    } catch (final Exception e) {
      e.printStackTrace();
    }

    // Create the main frame; center and show it.
    final TextEditor editor = new TextEditor();
    editor.setLocationRelativeTo(null);
    editor.setVisible(true);
  }
}

/**
 * The main frame of the application.  This frame provides a text editor
 * and a menu of options.
 */

final class TextEditor extends JFrame implements DocumentListener {

  /** The unique serialization number. */
  private static final long serialVersionUID = -4009854973936579007L;

  /** The file that is currently being edited, or null if this is a new file. */
  private File file;

  /**
   * The value <code>true</code> if the contents have been changed; otherwise,
   * <code>false</code>.
   */

  private boolean isChanged;

  /** The text area, which allows the user to edit text. */
  private final JTextArea textArea;

  /**
   * Initializes a new instance of the {@link TextEditor} class.
   */

  public TextEditor() {
    this.setDefaultCloseOperation(DISPOSE_ON_CLOSE);
    this.setSize(500, 500);

    // Determine the appropriate shortcut for the accelerators.
    final Toolkit toolkit = Toolkit.getDefaultToolkit();
    final int mask = toolkit.getMenuShortcutKeyMask();

    // Create the New menu item.
    final JMenuItem newMenu = new JMenuItem("New", KeyEvent.VK_N);
    newMenu.setAccelerator(KeyStroke.getKeyStroke(KeyEvent.VK_N, mask));
    newMenu.addActionListener(new ActionListener() {
      @Override
      public void actionPerformed(final ActionEvent arg0) {
        TextEditor.this.newFile();
      }
    });

    // Create the Open menu item.
    final JMenuItem openMenu = new JMenuItem("Open...", KeyEvent.VK_O);
    openMenu.setAccelerator(KeyStroke.getKeyStroke(KeyEvent.VK_O, mask));
    openMenu.addActionListener(new ActionListener() {
      @Override
      public void actionPerformed(final ActionEvent arg0) {
        TextEditor.this.openFile();
      }
    });

    // Create the Save menu item.
    final JMenuItem saveMenu = new JMenuItem("Save", KeyEvent.VK_S);
    saveMenu.setAccelerator(KeyStroke.getKeyStroke(KeyEvent.VK_S, mask));
    saveMenu.addActionListener(new ActionListener() {
      @Override
      public void actionPerformed(final ActionEvent arg0) {
        TextEditor.this.saveFile();
      }
    });

    // Create the Save As menu item.
    final JMenuItem saveAsMenu = new JMenuItem("Save As...", KeyEvent.VK_A);
    saveAsMenu.setAccelerator(KeyStroke.getKeyStroke(KeyEvent.VK_S, mask | InputEvent.SHIFT_MASK));
    saveAsMenu.addActionListener(new ActionListener() {
      @Override
      public void actionPerformed(final ActionEvent arg0) {
        TextEditor.this.saveAsFile();
      }
    });

    // Create the Exit menu item.
    final JMenuItem exitMenu = new JMenuItem("Exit", KeyEvent.VK_X);
    exitMenu.addActionListener(new ActionListener() {
      @Override
      public void actionPerformed(final ActionEvent arg0) {
        TextEditor.this.exit();
      }
    });

    // Create the File menu, and add each menu item in order.
    final JMenu menu = new JMenu("File");
    menu.setMnemonic(KeyEvent.VK_F);
    menu.add(newMenu);
    menu.add(openMenu);
    menu.addSeparator();
    menu.add(saveMenu);
    menu.add(saveAsMenu);
    menu.addSeparator();
    menu.add(exitMenu);

    // Add the menu bar that will appear at the top of the window.
    final JMenuBar menuBar = new JMenuBar();
    menuBar.add(menu);
    this.setJMenuBar(menuBar);

    // Add the text area, and use a fixed-width font. Listen for changes to the
    // document using this instance.
    this.textArea = new JTextArea();
    this.textArea.setFont(new Font("Monospaced", Font.PLAIN, 14));
    this.textArea.getDocument().addDocumentListener(this);

    // Wrap the editor pane in a scroll pane.
    this.setContentPane(new JScrollPane(this.textArea));

    // Initially update the title displayed in the window.
    this.updateFile(null);
  }

  @Override
  public void changedUpdate(final DocumentEvent arg0) {
    // When text has been changed in the document, set the flag that indicates
    // the contents have changed.
    this.isChanged = true;
  }

  /**
   * Exits the application. This method executes when the Exit menu item is
   * selected. This method prompts the user to save changes before proceeding.
   */

  void exit() {
    // Ask the user to save changes before disposing this instance, which in
    // turn closes and terminates the application.
    if (this.isConfirmedToProceed())
      this.dispose();
  }

  @Override
  public void insertUpdate(final DocumentEvent arg0) {
    // When text is inserted into the document, set the flag that indicates the
    // contents have changed.
    this.isChanged = true;
  }

  /**
   * Checks with the user if it okay to proceed with an operation that must
   * first save the contents of the file.  If the contents have not changed,
   * this method does nothing.  Otherwise, this method presents a dialog to the
   * user asking to save changes.  If the user does not want to save changes,
   * this method returns <code>true</code>.  Otherwise, this method will
   * attempt to save the contents to a file.  If there is an error saving the
   * file, a warning message is displayed, and this method returns
   * <code>false</code>.
   *
   * @return The value <code>true</code> if it okay to proceed with the
   * operation; otherwise, <code>false</code>.
   */

  private boolean isConfirmedToProceed() {
    // If the contents have not changed, then there is no need to save.
    if (!this.isChanged)
      return true;

    // Ask the user to save changes.
    final int selection = JOptionPane.showConfirmDialog(
        this,
        "This file has not been saved.  Would you like to save it?",
        "Save Changes?",
        JOptionPane.YES_NO_CANCEL_OPTION);

    // If the user selects YES, then save the file.  If the user selects NO,
    // then return true.  Otherwise, the user has selected CANCEL, and this
    // method returns false.
    switch (selection) {
    case JOptionPane.YES_OPTION:
      return this.saveFile();
    case JOptionPane.NO_OPTION:
      return true;
    default:
      return false;
    }
  }

  /**
   * Starts a new file. This method executes when the New menu item is
   * selected. This method prompts the user to save changes before proceeding.
   */

  void newFile() {
    // Ask the user to save changes before starting a new file.
    if (!this.isConfirmedToProceed())
      return;

    // Clear the contents, and update the title shown in the window.
    this.textArea.setText("");
    this.updateFile(null);
  }

  /**
   * Opens a new file. This method executes when the Open menu item is
   * selected. This method prompts the user to save changes before proceeding.
   */

  void openFile() {
    // Ask the user to save changes before opening a different file.
    if (!this.isConfirmedToProceed())
      return;

    // Show the standard Open Dialog to select a file.
    final JFileChooser fileChooser = new JFileChooser();
    if (fileChooser.showOpenDialog(this) != JFileChooser.APPROVE_OPTION)
      return;

    // Catch I/O errors while opening or reading the file.
    try {
      // Open the file, and read all bytes into an array.
      final File selectedFile = fileChooser.getSelectedFile();
      final ByteArrayOutputStream stream = new ByteArrayOutputStream();
      final FileInputStream reader = new FileInputStream(selectedFile);
      try {
        final byte[] buffer = new byte[1024];
        int numRead;
        while (-1 != (numRead = reader.read(buffer)))
          stream.write(buffer, 0, numRead);

        // Set the text from the array into the text area, and update the title
        // shown in the dialog.
        this.textArea.setText(stream.toString());
        this.updateFile(selectedFile);

      } finally {
        // Always close the file.
        reader.close();
      }

    } catch (final IOException e) {
      // Show an error message if there are errors opening the file.
      this.showErrorDialog("Error Opening File", e);
    }
  }

  @Override
  public void removeUpdate(final DocumentEvent arg0) {
    // When text has been removed from the document, set the flag that
    // indicates the contents have changed.
    this.isChanged = true;
  }

  /**
   * Saves the contents of the text area to the specified file and updates the
   * title of the window accordingly.  If the method completes successfully,
   * it returns <code>true</code>; otherwise, it presents an error message to
   * the user and returns <code>false</code>.
   *
   * @param newFile The path to the file to save.
   *
   * @return The value <code>true</code> if the file is saved successfully;
   * otherwise, <code>false</code>.
   */

  private boolean save(final File newFile) {
    assert null != newFile;

    // Catch I/O errors while creating or writing the file.
    try {
      // Write all text from the text area to the specified path.
      final FileWriter writer = new FileWriter(newFile);
      try {
        writer.write(this.textArea.getText());
      } finally {
        // Always close the file.
        writer.close();
      }

      // Update the title of the window and return true to indicate success.
      this.updateFile(newFile);
      return true;

    } catch (final IOException e) {
      // Show an error message if there are errors saving the file.
      this.showErrorDialog("Error Saving File", e);
      return false;
    }
  }

  /**
   * Saves the contents in the text area to a file selected by the user.
   *
   * @return The value <code>true</code> if the file was saved successfully;
   * otherwise, <code>false</code>.
   */

  boolean saveAsFile() {
    // Show the Save Dialog to the user; if the user cancels, return false.
    final JFileChooser fileChooser = new JFileChooser();
    if (fileChooser.showSaveDialog(this) != JFileChooser.APPROVE_OPTION)
      return false;

    // Otherwise, save the file with the specified path.
    final File selectedFile = fileChooser.getSelectedFile();
    return this.save(selectedFile);
  }

  /**
   * Saves the contents in the text area to the current file.  If this is a new
   * document, the user is shown a dialog to select a path.
   *
   * @return The value <code>true</code> if the file was saved successfully;
   * otherwise, <code>false</code>.
   */

  boolean saveFile() {
    // If this is a new file, then show the dialog.
    if (this.file == null)
      return this.saveAsFile();

    // Otherwise, save the file with the same path.
    return this.save(this.file);
  }

  /**
   * Shows an error dialog.  This method is executed when there is an
   * error reading or writing a file.
   *
   * @param title The title to display in the dialog.
   * @param error The exception used for the error message text.
   */

  private void showErrorDialog(final String title, final Exception error) {
    assert null != title;
    assert null != error;

    JOptionPane.showMessageDialog(this,
        error.getMessage(),
        title,
        JOptionPane.ERROR_MESSAGE);
  }

  /**
   * Updates the {@link #file} field with a new value, clears the {@link
   * #isChanged} flag, and updates the title of the window based on the name of
   * the file.
   *
   * @param newFile
   */

  private void updateFile(final File newFile) {
    this.file = newFile;
    this.isChanged = false;

    final String name = newFile == null ? "Untitled" : newFile.getName();
    this.setTitle(name + " - CSCI 2912 Editor");
  }
}