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");
	}
}