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