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