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.UndoableEditEventThe 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.WindowListenerThis 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.