Ray Toal
Loyola Marymount University
and Ticketmaster
2002-05-01
A GUI application will have
The AWT is the "library" containing layout management, event and listener management, as well as fonts, colors, points, dimensions, timers, borders, icons, and other useful stuff.
Swing is the "library" with the components, and a small number of extra events and layout tools that Sun did not put into the AWT. (The AWT has some components but DON'T USE THEM.)
Time for a short tour of the components. We'll run
the demo that comes with the JDK; mine is at
/jdk1.3.1/demo/jfc/SwingSet2
Here is the complete component hierachy for JDK 1.3.1.
java.lang.Object java.awt.Component java.awt.Button java.awt.Canvas java.awt.Checkbox java.awt.Choice java.awt.Label java.awt.List java.awt.Scrollbar java.awt.TextComponent java.awt.TextField java.awt.TextArea javax.swing.Box.Filler java.awt.Container java.awt.Panel java.applet.Applet javax.swing.JApplet java.awt.Window java.awt.Frame javax.swing.JFrame java.awt.Dialog java.awt.FileDialog javax.swing.JDialog javax.swing.JWindow javax.swing.BasicToolBarUI.DragWindow java.awt.ScrollPane javax.swing.Box javax.swing.CellRendererPane javax.swing.plaf.basic.BasicSplitPaneDivider javax.swing.tree.DefaultTreeCellEditor.EditorContainer javax.swing.JComponent javax.swing.AbstractButton javax.swing.JButton javax.swing.JMenuItem javax.swing.JCheckBoxMenuItem javax.swing.JMenu javax.swing.JRadioButtonMenuItem javax.swing.JToggleButton javax.swing.JCheckBox javax.swing.JRadioButton javax.swing.JColorChooser javax.swing.JComboBox javax.swing.JFileChooser javax.swing.JInternalFrame javax.swing.JInternalFrame.JDesktopIcon javax.swing.JLabel javax.swing.JLayeredPane javax.swing.JDesktopPane javax.swing.JList javax.swing.JMenuBar javax.swing.JOptionPane javax.swing.JPanel javax.swing.JPopupMenu javax.swing.JProgressBar javax.swing.JRootPane javax.swing.JScrollBar javax.swing.JScrollPane.ScrollBar javax.swing.JScrollPane javax.swing.JSeparator javax.swing.JPopupMenu.Separator javax.swing.JToolBar.Separator javax.swing.JSlider javax.swing.JSplitPane javax.swing.JTabbedPane javax.swing.JTable javax.swing.JTableHeader javax.swing.text.JTextComponent javax.swing.JEditorPane javax.swing.JTextPane javax.swing.JTextArea javax.swing.JTextField javax.swing.JPasswordField javax.swing.JToolBar javax.swing.JToolTip javax.swing.JTree javax.swing.JViewport
Normally the state of a component is stored in the component's
model, for example a JButton
will have a
ButtonModel
that you can set with setModel()
and retrieve with getModel()
. You often
write code like "button.getModel().setSelected(true)
" though
sometimes, Swing defines methods directly on the components that
are passed through to the models.
Here is the complete list of models
ListModel | JList |
ComboBoxModel | JComboBox |
ButtonModel | JButton, JToggleButton, JCheckBox, JRadioButton, JMenu, JMenuItem, JCheckBoxMenuItem, JRadioButtonMenuItem |
Document | JTextField, JPasswordField, JTextArea, JEditorPane, JTextPane |
BoundedRangeModel | JProgressBar, JScrollBar, JSlider |
SingleSelectionModel | JMenuBar, JPopupMenu, JTabbedPane |
TableModel | JTable |
TableColumnModel | JTable |
TreeModel | JTree |
TreeSelectionModel | JTree |
When you do something to a component it fires an event. Here is the complete hierarchy of events
java.lang.Object java.util.EventObject java.awt.AWTEvent java.awt.event.ActionEvent java.awt.event.AdjustmentEvent javax.swing.event.AncestorEvent java.awt.event.ComponentEvent java.awt.event.ContainerEvent java.awt.event.FocusEvent java.awt.event.InputEvent java.awt.event.KeyEvent javax.swing.event.MenuKeyEvent java.awt.event.MouseEvent javax.swing.event.MenuDragMouseEvent java.awt.event.PaintEvent java.awt.event.WindowEvent java.awt.event.HierarchyEvent java.awt.event.InputMethodEvent javax.swing.event.InternalFrameEvent java.awt.event.InvocationEvent java.awt.event.ItemEvent java.awt.event.TextEvent java.beans.PropertyChangeEvent javax.swing.event.CaretEvent javax.swing.event.ChangeEvent javax.swing.event.HyperlinkEvent javax.swing.event.ListDataEvent javax.swing.event.ListSelectionEvent javax.swing.event.MenuEvent javax.swing.event.PopupMenuEvent javax.swing.event.TableColumnModelEvent javax.swing.event.TableModelEvent javax.swing.event.TreeExpansionEvent javax.swing.event.TreeModelEvent javax.swing.event.TreeSelectionEvent javax.swing.event.UndoableEditEvent
The event is picked up by a listener. Here is the complete hierarchy of listeners
java.util.EventListener java.awt.event.ActionListener java.awt.event.AdjustmentListener javax.swing.event.AncestorListener java.awt.event.AWTEventListener javax.swing.event.CaretListener javax.swing.event.CellEditorListener javax.swing.event.ChangeListener java.awt.event.ComponentListener java.awt.event.ContainerListener javax.swing.event.DocumentListener java.awt.event.FocusListener java.awt.event.HierarchyBoundsListener java.awt.event.HierarchyListener javax.swing.event.HyperlinkListener java.awt.event.InputMethodListener java.awt.event.ItemListener javax.swing.event.InternalFrameListener java.awt.event.KeyListener javax.swing.event.ListDataListener javax.swing.event.ListSelectionListener javax.swing.event.MenuDragMouseListener javax.swing.event.MenuKeyListener javax.swing.event.MenuListener java.awt.event.MouseListener javax.swing.event.MouseInputListener java.awt.event.MouseMotionListener javax.swing.event.MouseInputListener javax.swing.event.PopupMenuListener java.beans.PropertyChangeListener javax.swing.event.TableColumnModelListener javax.swing.event.TableModelListener java.awt.event.TextListener javax.swing.event.TreeExpansionListener javax.swing.event.TreeModelListener javax.swing.event.TreeSelectionListener javax.swing.event.TreeWillExpandListener javax.swing.event.UndoableEditListener java.awt.event.WindowListener
This is a classic program in which each ball is a thread and has two listeners that listen to the Faster and Slower buttons.
Many Java "gurus" suggest that you use "anonymous inner classes" for listners, so I did that here... BUT... then you can't remove them from the buttons' listener lists. The threads stay in memory, even after they stop running. Here is the awful code:
import java.util.List;
import java.util.ArrayList;
import java.util.Collections;
import java.util.Iterator;
import java.util.Date;
import java.awt.Graphics;
import java.awt.Color;
import java.awt.Image;
import java.awt.event.ActionListener;
import java.awt.event.ActionEvent;
import javax.swing.JFrame;
import javax.swing.JPanel;
import javax.swing.JButton;
/**
* A demostration of memory leaks that occur when you fail to remove
* a listener.
*/
public class MemoryLeakDemo extends JFrame implements Runnable {
private JPanel canvas = new JPanel();
private JPanel buttonPanel = new JPanel();
private JButton launchButton = new JButton("Launch");
private JButton fasterButton = new JButton("Faster");
private JButton slowerButton = new JButton("Slower");
/**
* A thread to do the rendering on, since we don't want to render
* on the user interface thread.
*/
Thread renderThread = new Thread(this);
/**
* Stores all the ball threads.
*/
List balls = Collections.synchronizedList(new ArrayList());
/**
* Constructs and lays out the application frame.
*/
public MemoryLeakDemo() {
launchButton.addActionListener(new ActionListener() {
public void actionPerformed(ActionEvent e) {
Ball ball = new Ball();
balls.add(ball);
ball.start();
}
});
buttonPanel.add(launchButton);
buttonPanel.add(fasterButton);
buttonPanel.add(slowerButton);
getContentPane().add(buttonPanel, "South");
getContentPane().add(canvas, "Center");
setResizable(false);
}
/**
* Renders the canvas with all the balls. First it creates a back
* buffer and graphics objects for the back buffer and the canvas.
* Then it loops around drawing all the live balls to the back
* buffer then blitting the back buffer to the canvas.
*/
public void run() {
Image buffer = createImage(canvas.getWidth(), canvas.getHeight());
Graphics g = buffer.getGraphics();
Graphics gc = canvas.getGraphics();
while (true) {
g.setColor(Color.white);
g.fillRect(0, 0, canvas.getWidth(), canvas.getHeight());
synchronized (balls) {
for (Iterator i = balls.iterator(); i.hasNext();) {
Ball b = (Ball)i.next();
b.draw(g);
}
}
gc.drawImage(buffer, 0, 0, canvas);
try {Thread.sleep(5);} catch(InterruptedException e) {}
}
}
/**
* Runs the demonstration as an application.
*/
public static void main(String[] args) {
MemoryLeakDemo bouncer = new MemoryLeakDemo();
bouncer.setTitle("Bouncing Balls");
bouncer.setSize(400, 300);
bouncer.show();
bouncer.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
bouncer.renderThread.setPriority(Thread.MAX_PRIORITY);
bouncer.renderThread.start();
}
/**
* A ball is something that bounces around on a JPanel.
*/
class Ball extends Thread implements Cloneable {
private Color color = Color.black;
private int x = 0;
private int y = 0;
private int dx = 2;
private int dy = 2;
private int delay = 10;
private static final int RADIUS = 10;
public Ball() {
slowerButton.addActionListener(new ActionListener() {
public void actionPerformed(ActionEvent e) {
delay++;
}
});
fasterButton.addActionListener(new ActionListener() {
public void actionPerformed(ActionEvent e) {
if (delay > 5) delay--;
}
});
}
/**
* Draws the ball as a filled in circle.
*/
public void draw(Graphics g) {
g.setColor(color);
g.fillOval(x, y, RADIUS, RADIUS);
}
/**
* Updates the position of the ball, taking care of bounces.
*/
protected void move() {
x += dx;
y += dy;
if (x < 0) {
// Bounce off left wall.
x = 0; dx = -dx;
}
if (x + RADIUS >= canvas.getWidth()) {
// Bounce off right wall.
x = canvas.getWidth() - RADIUS; dx = -dx;
}
if (y < 0) {
// Bounce off top wall.
y = 0; dy = -dy;
}
if (y + RADIUS >= canvas.getHeight()) {
// Bounce off bottom wall.
y = canvas.getHeight() - RADIUS; dy = -dy;
}
}
/**
* A ball basically just moves 1000 times. After each position update,
* it sleeps for a few milliseconds.
*/
public void run() {
for (int i = 1; i <= 2000; i++) {
move();
try {Thread.sleep(delay);} catch(InterruptedException e) {}
}
balls.remove(this);
}
}
}
This is bad for two reasons: as we add more balls, the program slows down because (1) memory growth -- more and more crap in memory, and (2) a useless increase in the number of dispatchings.
Let's demonstrate the program running under OptimizeIt
After running all the threads and watching them stop, we see they are still there
Tracing the references back to the "roots" we find they are sitting on listener lists of buttons:
We just have to name the listener objects and remove them after the threads finish.
import java.util.List; import java.util.ArrayList; import java.util.Collections; import java.util.Iterator; import java.util.Date; import java.awt.Graphics; import java.awt.Color; import java.awt.Image; import java.awt.event.ActionListener; import java.awt.event.ActionEvent; import javax.swing.JFrame; import javax.swing.JPanel; import javax.swing.JButton; /** * A demostration of memory leaks that occur when you fail to remove * a listener. */ public class FixedLeakDemo extends JFrame implements Runnable { private JPanel canvas = new JPanel(); private JPanel buttonPanel = new JPanel(); private JButton launchButton = new JButton("Launch"); private JButton fasterButton = new JButton("Faster"); private JButton slowerButton = new JButton("Slower"); /** * A thread to do the rendering on, since we don't want to render * on the user interface thread. */ Thread renderThread = new Thread(this); /** * Stores all the ball threads. */ List balls = Collections.synchronizedList(new ArrayList()); /** * Constructs and lays out the application frame. */ public FixedLeakDemo() { launchButton.addActionListener(new ActionListener() { public void actionPerformed(ActionEvent e) { Ball ball = new Ball(); balls.add(ball); ball.start(); } }); buttonPanel.add(launchButton); buttonPanel.add(fasterButton); buttonPanel.add(slowerButton); getContentPane().add(buttonPanel, "South"); getContentPane().add(canvas, "Center"); setResizable(false); } /** * Renders the canvas with all the balls. First it creates a back * buffer and graphics objects for the back buffer and the canvas. * Then it loops around drawing all the live balls to the back * buffer then blitting the back buffer to the canvas. */ public void run() { Image buffer = createImage(canvas.getWidth(), canvas.getHeight()); Graphics g = buffer.getGraphics(); Graphics gc = canvas.getGraphics(); while (true) { g.setColor(Color.white); g.fillRect(0, 0, canvas.getWidth(), canvas.getHeight()); synchronized (balls) { for (Iterator i = balls.iterator(); i.hasNext();) { Ball b = (Ball)i.next(); b.draw(g); } } gc.drawImage(buffer, 0, 0, canvas); try {Thread.sleep(5);} catch(InterruptedException e) {} } } /** * Runs the demonstration as an application. */ public static void main(String[] args) { FixedLeakDemo bouncer = new FixedLeakDemo(); bouncer.setTitle("Bouncing Balls"); bouncer.setSize(400, 300); bouncer.show(); bouncer.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE); bouncer.renderThread.setPriority(Thread.MAX_PRIORITY); bouncer.renderThread.start(); } /** * A ball is something that bounces around on a JPanel. */ class Ball extends Thread implements Cloneable { private Color color = Color.black; private int x = 0; private int y = 0; private int dx = 2; private int dy = 2; private int delay = 10; private static final int RADIUS = 10; ActionListener slowerButtonListener = new ActionListener() { public void actionPerformed(ActionEvent e) { delay++; } }; ActionListener fasterButtonListener = new ActionListener() { public void actionPerformed(ActionEvent e) { if (delay > 5) delay--; } }; public Ball() { slowerButton.addActionListener(slowerButtonListener); fasterButton.addActionListener(fasterButtonListener); } /** * Draws the ball as a filled in circle. */ public void draw(Graphics g) { g.setColor(color); g.fillOval(x, y, RADIUS, RADIUS); } /** * Updates the position of the ball, taking care of bounces. */ protected void move() { x += dx; y += dy; if (x < 0) { // Bounce off left wall. x = 0; dx = -dx; } if (x + RADIUS >= canvas.getWidth()) { // Bounce off right wall. x = canvas.getWidth() - RADIUS; dx = -dx; } if (y < 0) { // Bounce off top wall. y = 0; dy = -dy; } if (y + RADIUS >= canvas.getHeight()) { // Bounce off bottom wall. y = canvas.getHeight() - RADIUS; dy = -dy; } } /** * A ball basically just moves 1000 times. After each position update, * it sleeps for a few milliseconds. */ public void run() { for (int i = 1; i <= 2000; i++) { move(); try {Thread.sleep(delay);} catch(InterruptedException e) {} } balls.remove(this); fasterButton.removeActionListener(fasterButtonListener); slowerButton.removeActionListener(slowerButtonListener); } } }
Java memory leaks occur when you have objects that are reachable but you don't need or want them anymore -- they are more properly called loiterers.
The Java garbage collector takes care of the unreachable objects, which are the real memory leaks of C.
However even if you recognize loiterers you might not be able to do anything since the reference may be a private field of a class for which you do not have the source code.
Java memory leaks are less frequent than those in C or C++ but are usually much more severe.
Henry and Lycklama classify loiterers into four groups
Swing gives you some basic support for undo, but only for document objects.
In a complex UI, you may need:
Here is a demonstration of these
import java.util.Vector; import javax.swing.*; import javax.swing.text.*; import javax.swing.table.*; import java.awt.*; import java.awt.event.*; import javax.swing.event.*; import javax.swing.undo.*; import java.beans.*; /** * Application demonstrating how multiple components can share * an undo manager and have all undos and redos set focus to * the affected JComponent. */ public class UndoDemo extends JFrame { UndoMediator mediator; public UndoDemo() { super("Undo Demo"); UndoMediator mediator = new UndoMediator(); JTabbedPane tabbedPane = new JTabbedPane(); tabbedPane.addTab("One", new DemoPanel(mediator, tabbedPane, "One")); tabbedPane.addTab("Two", new DemoPanel(mediator, tabbedPane, "Two")); getContentPane().add(tabbedPane); addKeyListener(mediator); } public static void main(String[] args) { JFrame frame = new UndoDemoMainFrame(); frame.setSize(600, 400); frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE); frame.show(); } } class DemoPanel extends JPanel { JLabel helpLabel = new JLabel("Ctrl+Z to Undo, Ctrl+Y to Redo"); JTextArea area = new JTextArea(); JList list = new JList(new DefaultListModel()); JButton addToListButton = new JButton("Add"); JButton removeFromListButton = new JButton("Remove"); JCheckBox checkBox = new JCheckBox("Hello"); JTextPane textPane = new JTextPane(); JButton boldButton = new JButton("Bold"); JButton italicButton = new JButton("Italic"); JTable table = new JTable(); JButton addToTableButton = new JButton("Add"); JButton removeFromTableButton = new JButton("Remove"); /** * */ public DemoPanel(final UndoMediator mediator, final JTabbedPane tabbedPane, final String tabName) { setLayout(new BorderLayout()); add(helpLabel, "North"); JPanel panel = new JPanel(); panel.setLayout(new GridLayout(0, 1)); // An example JTextArea with a PlainDocument panel.add(new JScrollPane(area)); mediator.registerDocument(area.getDocument(), area, tabbedPane, tabName); // An example JList JPanel panel2 = new JPanel(); panel2.setLayout(new BorderLayout()); panel2.add(new JScrollPane(list), "Center"); panel2.add(addToListButton, "West"); addToListButton.addActionListener(new ActionListener() { public void actionPerformed(ActionEvent e) { DefaultListModel model = (DefaultListModel)list.getModel(); String newEntry = String.valueOf(Math.random()*100); mediator.addToListModel(model, model.getSize(), newEntry, list, tabbedPane, tabName); } }); panel2.add(removeFromListButton, "East"); removeFromListButton.addActionListener(new ActionListener() { public void actionPerformed(ActionEvent e) { DefaultListModel model = (DefaultListModel)list.getModel(); int index = list.getSelectedIndex(); if (index != -1) { mediator.removeFromListModel(model, index, list, tabbedPane, tabName); } } }); panel.add(panel2); // An example JTextPane JPanel panel3 = new JPanel(); panel3.setLayout(new BorderLayout()); panel3.add(new JScrollPane(textPane), "Center"); mediator.registerDocument(textPane.getDocument(), textPane, tabbedPane, tabName); panel3.add(boldButton, "West"); boldButton.addActionListener(new StyledEditorKit.BoldAction()); panel3.add(italicButton, "East"); italicButton.addActionListener(new StyledEditorKit.ItalicAction()); panel.add(panel3); // An example JTable JPanel panel4 = new JPanel(); panel4.setLayout(new BorderLayout()); panel4.add(new JScrollPane(table), "Center"); panel4.add(addToTableButton, "West"); addToTableButton.addActionListener(new ActionListener() { public void actionPerformed(ActionEvent e) { DefaultTableModel model = (DefaultTableModel)table.getModel(); Vector data = new Vector(model.getColumnCount()); for (int i = 0; i < model.getColumnCount(); i++) { data.add(String.valueOf((int)(Math.random()*10000))); } mediator.addRowToTableModel(model, model.getRowCount(), data, table, tabbedPane, tabName); } }); panel4.add(removeFromTableButton, "East"); removeFromTableButton.addActionListener(new ActionListener() { public void actionPerformed(ActionEvent e) { DefaultTableModel model = (DefaultTableModel)table.getModel(); ListSelectionModel selectionModel = table.getSelectionModel(); int index = selectionModel.getMinSelectionIndex(); if (index != -1) { mediator.removeRowFromTableModel(model, index, table, tabbedPane, tabName); } } }); table.setModel(new DefaultTableModel( new String[]{"a", "bee", "sea", "dee", "eeeee"}, 0)); panel.add(panel4); add(panel, "Center"); // An example JCheckBox add(checkBox, "South"); mediator.registerCheckBoxForUndoableSelects(checkBox, tabbedPane, tabName); } } /** * A mediator object that coordinates undo and redo requests among * multiple components; it also provides direct methods for modifying * components that record the effects in edit objects which are * automatically registered for undo/redo. Use this class as * follows: * * 1. For components whose models are documents, such as JTextFields, * JTextAreas, JTextPanes, etc. call * * registerDocument(document, focusInfo) * * Swing already has documents generating their own undoable edit * events, therefore all one has to do is register a document. * For other objects, we can't get away with this. We need a way * for these other objects to generate UndoableEditEvents. * * 2. For JLists, we can't just register a model, since list models * don't automatically fire UndoableEditEvents. Instead, clients * have to call special methods to update the model that will * then fire these events. * * addToListModel(model, index, data, focusInfo) * removeFromListModel(model, index, focusInfo) * * 3. JTables work like JLists: * * addRowToTableModel(model, index, data, focusInfo) * removeRowFromTableModel(model, index, focusInfo) * * 4. Checkboxes are a little different since it's not the model * that we need to examine events on; it's the checkbox itself. * Fortunately, we can register the checkbox with the mediator and * examine the model on the fly. Clients call: * * registerCheckBoxForUndoableSelects(checkbox, focusInfo) */ class UndoMediator implements KeyListener { /** * The one and only undo manager. This is the manager to which * you add edit objects when actions occur, and on which you * actually call undo and redo. */ private UndoManager manager = new UndoManager(); /** * Ugh - hard code the undo limit since this is a throw away demo. */ public UndoMediator() { manager.setLimit(1000); } /** * Super convenient method for taking advantage of the fact * that documents always fire undoable edit events: this * method simply registers a document with the mediator * so that all undoable edit events fired by the * document will be handled here automatically. */ public void registerDocument(Document document, final JComponent component, final JTabbedPane tabbedPane, final String tabName) { document.addUndoableEditListener(new UndoableEditListener() { public void undoableEditHappened(UndoableEditEvent e) { System.out.println(e.getEdit()); manager.addEdit(new DocumentEdit(e.getEdit(), new FocusInfo(component, tabbedPane, tabName))); } }); } /** * */ public void registerCheckBoxForUndoableSelects( final JCheckBox checkBox, final JTabbedPane tabbedPane, final String tabName) { checkBox.addActionListener(new ActionListener() { public void actionPerformed(ActionEvent e) { ButtonModel model = checkBox.getModel(); manager.addEdit(new ButtonToggleEdit( model, model.isSelected(), new FocusInfo(checkBox, tabbedPane, tabName))); } }); } // Utilities for lists and comboboxes. /** * Adds an item to a DefaultListModel and automatically records the * edit so that it can be undone or redone. */ public void addToListModel(DefaultListModel model, int index, Object element, final JComponent component, final JTabbedPane tabbedPane, final String tabName) { manager.addEdit(new ListAddEdit( model, index, element, new FocusInfo(component, tabbedPane, tabName))); model.add(index, element); } /** * Removes an item to a DefaultListModel and automatically records the * edit so that it can be undone or redone. */ public void removeFromListModel(DefaultListModel model, int index, final JComponent component, final JTabbedPane tabbedPane, final String tabName) { Object element = model.get(index); manager.addEdit(new ListRemoveEdit( model, index, element, new FocusInfo(component, tabbedPane, tabName))); model.remove(index); } public void addRowToTableModel(DefaultTableModel model, int index, Vector data, JTable table, JTabbedPane tabbedPane, String tabName) { manager.addEdit(new TableAddRowEdit(model, index, data, new FocusInfo(table, tabbedPane, tabName))); model.insertRow(index, data); } public void removeRowFromTableModel(DefaultTableModel model, int index, JTable table, JTabbedPane tabbedPane, String tabName) { Vector data = new Vector(model.getColumnCount()); // Believe it or not, there is NO method in the DefaultTableModel // class to obtain the value of a row! We have to get it the // hard way. for (int i = 0; i < model.getColumnCount(); i++) { data.add(model.getValueAt(index, i)); } manager.addEdit(new TableRemoveRowEdit(model, index, data, new FocusInfo(table, tabbedPane, tabName))); model.removeRow(index); } // KeyListener methods. Allows Ctrl+Z and Ctrl+Y to be handled. public void keyPressed(KeyEvent e) { if ((e.getKeyCode() == KeyEvent.VK_Z) && (e.isControlDown())) { try { manager.undo(); } catch(CannotUndoException cue) { Toolkit.getDefaultToolkit().beep(); } } if ((e.getKeyCode() == KeyEvent.VK_Y) && (e.isControlDown())) { try { manager.redo(); } catch(CannotRedoException cue) { Toolkit.getDefaultToolkit().beep(); } } } public void keyReleased(KeyEvent e) {} public void keyTyped(KeyEvent e) {} /** * An inner class for collecting together information necessary to * focus a component that may be on a different tabbed pane than * the one that is currently visible. The class is fairly simple: * it contains only (1) a component that you want to give focus to, * and (2) the tabbed pane and tab name, if any, that it is on. * Note that it is not necessary to have a tabbed pane at all, * but if you do, everything will be taken care of. */ public static class FocusInfo { private JComponent component; private JTabbedPane tabbedPane; private String tabName; public FocusInfo(JComponent c, JTabbedPane p, String s) { this.component = c; this.tabbedPane = p; this.tabName = s; } public FocusInfo(JComponent c) { this(c, null, null); } /** * Sets the focus to the desired component, after selecting the * desired tab, if necessary. */ public void doFocus() { if (tabbedPane != null) { int index = tabbedPane.indexOfTab(tabName); if (index != -1) { if (index != tabbedPane.getSelectedIndex()) { tabbedPane.setSelectedIndex(index); } } } if (component != null) { component.requestFocus(); } } } } // // The edit objects. // /** * An edit object that stores a component for refocusing to. * Make subclasses of this class and arrange for their undo * and redo methods to call super.undo() and super.redo(). */ class ComponentAwareEdit extends AbstractUndoableEdit { private UndoMediator.FocusInfo focusInfo; public ComponentAwareEdit(UndoMediator.FocusInfo focusInfo) { this.focusInfo = focusInfo; } public void undo() throws CannotUndoException { focusInfo.doFocus(); } public void redo() throws CannotRedoException { focusInfo.doFocus(); } public boolean canUndo() {return true;} public boolean canRedo() {return true;} } /** * An edit object for adding an item to a DefaultListModel. */ class ListAddEdit extends ComponentAwareEdit { private DefaultListModel model; private int index; private Object element; public ListAddEdit(DefaultListModel model, int index, Object element, UndoMediator.FocusInfo focusInfo) { super(focusInfo); this.model = model; this.index = index; this.element = element; } public void undo() throws CannotUndoException { super.undo(); model.removeElementAt(index); } public void redo() throws CannotRedoException { super.redo(); model.insertElementAt(element, index); } } /** * An edit object for removing an item from a DefaultListModel. */ class ListRemoveEdit extends ComponentAwareEdit { private DefaultListModel model; private int index; private Object element; public ListRemoveEdit(DefaultListModel model, int index, Object element, UndoMediator.FocusInfo focusInfo) { super(focusInfo); this.model = model; this.index = index; this.element = element; } public void undo() throws CannotUndoException { super.undo(); model.insertElementAt(element, index); } public void redo() throws CannotRedoException { super.redo(); model.removeElementAt(index); } } /** * An edit object for adding an item to a DefaultTableModel. */ class TableAddRowEdit extends ComponentAwareEdit { private DefaultTableModel model; private int index; private Vector data; public TableAddRowEdit(DefaultTableModel model, int index, Vector data, UndoMediator.FocusInfo focusInfo) { super(focusInfo); this.model = model; this.index = index; this.data = data; } public void undo() throws CannotUndoException { super.undo(); model.removeRow(index); } public void redo() throws CannotRedoException { super.redo(); model.insertRow(index, data); } } /** * An edit object for removing an item from a DefaultTableModel. */ class TableRemoveRowEdit extends ComponentAwareEdit { private DefaultTableModel model; private int index; private Vector data; public TableRemoveRowEdit(DefaultTableModel model, int index, Vector data, UndoMediator.FocusInfo focusInfo) { super(focusInfo); this.model = model; this.index = index; this.data = data; } public void undo() throws CannotUndoException { super.undo(); model.insertRow(index, data); } public void redo() throws CannotRedoException { super.redo(); model.removeRow(index); } } /** * An edit object for toggling the selected state of a button, * checkbox, etc. */ class ButtonToggleEdit extends ComponentAwareEdit { private ButtonModel model; private boolean selected; public ButtonToggleEdit(ButtonModel model, boolean selected, UndoMediator.FocusInfo focusInfo) { super(focusInfo); this.model = model; this.selected = selected; } public void undo() throws CannotUndoException { super.undo(); model.setSelected(!selected); } public void redo() throws CannotRedoException { super.redo(); model.setSelected(selected); } } /** * A wrapper edit object that encapsulates another edit. The idea is * that whenever this edit is asked to be undone or redone, it first * sets focus as directed by a FocusInfo object then undoes or redos by * delegating to the internal edit object. */ class DocumentEdit extends ComponentAwareEdit { private UndoableEdit edit; public DocumentEdit(UndoableEdit edit, UndoMediator.FocusInfo focusInfo) { super(focusInfo); this.edit = edit; } public void undo() throws CannotUndoException { super.undo(); edit.undo(); } public void redo() throws CannotRedoException { super.redo(); edit.redo(); } }
Deciding to write a complex UI-based app in Java means you are starting with the adverse memory requirements of the JVM and the Swing libraries, so you must be careful not to make it worse.
Understand listeners --- in particular make sure you pair up your addListener and removeListener calls.
Spend a few hundred bucks on a good memory profiling tool because it will save you tens of thousands over the life of the product.