Swing’s JList is a very useful component for displaying lists of data. However, as noted in the previous section, it doesn’t provide a means for editing or manipulating its data model in any way. In this final step, we will write the new class, DragList, which extends JList to give it the ability to reorder its items by dragging one item at a time to a new position. We’ll deal with a bit of offscreen graphics and transparency along with a good dose of old-fashioned cleverness. Reapplying a pattern set by an earlier section, we will create the ReorderEvent and ReorderListener classes. We will also create a mechanism to notify objects that implement ReorderListener and are registered with the DragList whenever a ReorderEvent occurs.
The DragList class (example 14.14) is more complex than other GUI classes we have written, so we will discuss its logic thoroughly.
Example 14.14: chap14.gui5.DragList.java
1 package chap14.gui5; 2 3 import java.awt.AlphaComposite; 4 import java.awt.Component; 5 import java.awt.Composite; 6 import java.awt.Graphics; 7 import java.awt.Graphics2D; 8 import java.awt.Insets; 9 import java.awt.Point; 10 import java.awt.Rectangle; 11 import java.awt.event.MouseEvent; 12 import java.awt.image.BufferedImage; 13 import java.util.EventListener; 14 import java.util.EventObject; 15 import java.util.Iterator; 16 import java.util.Vector; 17 18 import javax.swing.DefaultListModel; 19 import javax.swing.JList; 20 import javax.swing.ListCellRenderer; 21 import javax.swing.ListModel; 22 import javax.swing.SwingUtilities; 23 import javax.swing.event.MouseInputListener; 24 25 public class DragList extends JList implements MouseInputListener { 26 27 private Object dragItem; 28 private int dragIndex = -1; 29 private BufferedImage dragImage; 30 private Rectangle dragRect = new Rectangle(); 31 private boolean inDrag = false; 32 33 private Point dragStart; 34 private int deltaY; 35 private int dragThreshold; 36 37 private boolean allowDrag; 38 39 private Vector listeners = new Vector(); 40 41 public DragList() { 42 this(new DefaultListModel()); 43 } 44 public DragList(DefaultListModel lm) { 45 super(lm); 46 addMouseListener(this); 47 addMouseMotionListener(this); 48 } 49 public void setModel(ListModel lm) { 50 super.setModel((DefaultListModel)lm); 51 } 52 public void setListData(Object[] listData) { 53 DefaultListModel model = (DefaultListModel)getModel(); 54 model.clear(); 55 for (int i = 0; i < listData.length; ++i) { 56 model.addElement(listData[i]); 57 } 58 } 59 public void setListData(Vector listData) { 60 DefaultListModel model = (DefaultListModel)getModel(); 61 model.clear(); 62 for (Iterator it = listData.iterator(); it.hasNext();) { 63 model.addElement(it.next()); 64 } 65 } 66 private void createDragImage() { 67 if (dragImage == null 68 || dragImage.getWidth() != dragRect.width 69 || dragImage.getHeight() != dragRect.height) { 70 dragImage = 71 new BufferedImage( 72 dragRect.width, 73 dragRect.height, 74 BufferedImage.TYPE_INT_RGB); 75 } 76 Graphics g = dragImage.getGraphics(); 77 78 ListCellRenderer renderer = getCellRenderer(); 79 Component comp = 80 renderer.getListCellRendererComponent( 81 this, 82 dragItem, 83 dragIndex, 84 true, 85 true); 86 SwingUtilities.paintComponent( 87 g, 88 comp, 89 this, 90 0, 91 0, 92 dragRect.width, 93 dragRect.height); 94 } 95 protected void paintComponent(Graphics g) { 96 super.paintComponent(g); 97 if (inDrag) { 98 Graphics2D g2 = (Graphics2D)g; 99 g2.setColor(getBackground()); 100 Rectangle r = getCellBounds(dragIndex, dragIndex); 101 g2.fillRect(r.x, r.y, r.width, r.height); 102 Composite saveComposite = g2.getComposite(); 103 g2.setComposite( 104 AlphaComposite.getInstance(AlphaComposite.SRC_OVER, 0.5f)); 105 g2.drawImage(dragImage, dragRect.x, dragRect.y, this); 106 g2.setComposite(saveComposite); 107 } 108 } 109 public void mousePressed(MouseEvent e) { 110 allowDrag = false; 111 if (!SwingUtilities.isLeftMouseButton(e)) { 112 return; 113 } 114 dragStart = e.getPoint(); 115 dragIndex = locationToIndex(dragStart); 116 if (dragIndex < 0) { 117 return; 118 } 119 dragRect = getCellBounds(dragIndex, dragIndex); 120 if (!dragRect.contains(dragStart)) { 121 clearSelection(); 122 return; 123 } 124 allowDrag = true; 125 DefaultListModel model = (DefaultListModel)getModel(); 126 dragItem = model.getElementAt(dragIndex); 127 dragThreshold = dragRect.height / 4; 128 deltaY = dragStart.y - dragRect.y; 129 } 130 public void mouseDragged(MouseEvent e) { 131 if (!allowDrag) { 132 return; 133 } 134 Point mouse = e.getPoint(); 135 if (!inDrag && Math.abs(mouse.y - dragStart.y) < dragThreshold) { 136 return; 137 } 138 if (!inDrag) { 139 clearSelection(); 140 createDragImage(); 141 inDrag = true; 142 } 143 //remember dragRect.y 144 int oldTop = dragRect.y; 145 146 dragRect.y = mouse.y - deltaY; 147 //dragRect is now at the accurate vertical location 148 //shift dragRect up or down as necessary so that it doesn't 149 //spill over the top or bottom of the DragList 150 Insets insets = getInsets(); 151 dragRect.y = Math.max(dragRect.y, insets.top); 152 dragRect.y = 153 Math.min(dragRect.y, getHeight() - dragRect.height - insets.bottom); 154 155 //index is the index of the item located at the vertical center of dragRect 156 int index = 157 locationToIndex(new Point(mouse.x, dragRect.y + dragRect.height / 2)); 158 if (dragIndex != index) { 159 DefaultListModel model = (DefaultListModel)getModel(); 160 //move dragItem to the new location in the list 161 model.remove(dragIndex); 162 model.add(index, dragItem); 163 dragIndex = index; 164 } 165 166 int minY = Math.min(dragRect.y, oldTop); 167 int maxY = Math.max(dragRect.y, oldTop); 168 repaint(dragRect.x, minY, dragRect.width, maxY + dragRect.height); 169 } 170 public void mouseReleased(MouseEvent e) { 171 if (inDrag) { 172 setSelectedIndex(dragIndex); 173 inDrag = false; 174 repaint(dragRect); 175 notifyListeners(new ReorderEvent(this)); 176 } 177 } 178 179 public void mouseClicked(MouseEvent e) {} 180 public void mouseMoved(MouseEvent e) {} 181 public void mouseEntered(MouseEvent e) {} 182 public void mouseExited(MouseEvent e) {} 183 184 public static class ReorderEvent extends EventObject { 185 public ReorderEvent(DragList dragList) { 186 super(dragList); 187 } 188 } 189 public static interface ReorderListener extends EventListener { 190 public void listReordered(ReorderEvent e); 191 } 192 public void addReorderListener(ReorderListener rl) { 193 listeners.add(rl); 194 } 195 public void removeReorderListener(ReorderListener rl) { 196 listeners.remove(rl); 197 } 198 private void notifyListeners(ReorderEvent event) { 199 for (Iterator it = listeners.iterator(); it.hasNext();) { 200 ReorderListener rl = (ReorderListener)it.next(); 201 rl.listReordered(event); 202 } 203 } 204 }
When the user presses the mouse in a list item and begins to drag it, DragList sets the object, dragItem, to this item and creates an image of its corresponding list cell, dragImage. It erases that list cell (by painting a blank rectangle over it) suggesting that the dragged item has vacated its initial position. While the mouse is dragged, DragList tracks the mouse’s motion, drawing dragImage semi-transparently over the contents of the list at the position of the mouse. Each time dragImage encroaches on a neighboring list cell, DragList switches dragItem and that list cell’s corresponding value in the ListModel. This change of order is automatically and visibly manifested in the DragList. For as long as the mouse is dragged, the reordering process continues as necessary and DragList continues to erase the dragged list cell wherever it may be at the moment. When the mouse is finally released, DragList erases dragImage and stops erasing dragItem’s list cell, thereby creating the illusion that the dragged cell has finally come to rest. Registered ReorderListeners are then notified that the list has been reordered.
Notice that the algorithm involves moving items’ positions within the list. While JList doesn’t provide methods for altering its data, it does provide access to its data model through the getModel() method which returns a ListModel. Unfortunately, the API for ListModel doesn’t provide methods for altering its data either, so we need to find a ListModel implementation that is editable. We could, of course, write our own class that implements ListModel and also supports editing, but we don’t need to. Starting with the javadocs for ListModel and searching via the “All Known Implementing Classes:” and “Direct Known Subclasses:” links we find that Swing provides the DefaultListModel class which provides a whole host of methods that support editing. The inheritance hierarchy for DefaultListModel is shown in Figure 14-10.
javax.swing.ListModel javax.swing.AbstractListModel javax.swing.DefaultListModel
Because having an editable ListModel is a requirement for DragList to be functional, we should ensure that DragList never uses anything but a DefaultListModel. We provide two constructors: one that takes no arguments and just creates a DefaultListModel for itself, and another that accepts a DefaultListModel argument to be used as its model. We also need to override any methods available from its superclass, JList, that might set the model to something other than a DefaultListModel. This requires us to override three methods. We override setModel(ListModel) to throw a ClassCastException if the argument is not a DefaultListModel, and we override setListData(Object[]) and setListData(Vector) to reuse DragList’s DefaultListModel by clearing it and adding the items in the array or Vector to it one by one.
Just as we did earlier with CheckboxListCell by creating a new event and listener type, we now create the ReorderEvent and ReorderListener for DragList. The ReorderEvent class defines the event of reordering the items in the list. The ReorderListener interface defines the one method listReordered(ReorderEvent). ReorderEvent’s only constructor takes a DragList as parameter so that it can set the event’s source as that DragList. DragList’s addReorderListener() method adds a ReorderListener to a private list of listeners and its removeReorderListener() method removes the specified ReorderListener from the list of listeners. DragList’s notifyListeners() method constructs a ReorderEvent and sends it to all registered ReorderListeners.
In the course of a mouse drag, three event types are always generated. These are in order:
a mouse press event when the user presses the mouse to begin the drag
a series of mouse drag events as the user drags the mouse
a mouse release event when the user releases the mouse thereby ending the drag
DragList implements MouseListener and writes mousePressed(), mouseDragged() and mouseReleased() event handlers that maintain several DragList attributes needed to achieve the visual effect of dragging. DragList registers itself as its own MouseListener.
The mousePressed() method initializes three attributes needed to create an image of the dragged item. These are dragIndex, dragItem and dragRect as shown in table 14-12.
Name | Meaning | How Obtained |
---|---|---|
dragIndex | The dragged item’s index in the list. | Obtained by passing the point of the mouse press into JList’s locationToIndex() method. |
dragItem | The item being dragged. | Obtained by passing dragIndex into the ListModel’s getElementAt() method. |
dragRect | The rectangular area that the DragList will use to paint the dragged item. | Obtained by passing dragIndex into JList’s getCell-Bounds() method. |
The mousePressed() method initializes three attributes needed for the mouseDragged() method to determine whether an item is being dragged or not. These are dragStart, dragThreshold and allowDrag as shown in table 14-13.
Name | Meaning | Initialization |
---|---|---|
dragStart | The location of the mousePress. | Set to MouseEvent.getPoint(). |
dragThreshold | The maximum vertical distance the mouse can be dragged before dragItem should be considered to be dragged. This eliminates unwanted drags due to inadvertent mouse jiggles. | Set to 1/4 the height of dragRect. |
allowDrag | Whether or not dragging should be allowed. | Set to true if the mouse pressed event meets certain criteria. |
Two other attributes are needed to enable the dragging process. These are deltaY and inDrag as shown in table 14-14.
Interested Methods | ||||
---|---|---|---|---|
Name | Meaning | mousePressed() | mouseDragged() | mouseReleased() |
deltaY | Where, with respect to the mouse position, dragImage should be painted as the mouse is dragged. | Sets to the vertical distance between dragStart, and the top of the list item containing dragStart. | Uses deltaY to update dragRect’s position ensuring that the top of dragRect is always the same vertical distance from the mouse location. | N/A |
inDrag | Whether or not an item is in the process of being dragged. | N/A | Sets to true only if allowDrag is true, the drag is with the left-mouse button and the mouse has moved at least dragThreshold pixels vertically away from dragStart. | Sets to false. |
As shown in table 14-15, the paintComponent(), mouseDragged() and mouseReleased() methods do different things depending on the value of inDrag.
Interested Methods | |||
---|---|---|---|
Value | paintComponent() | mouseDragged() | mouseReleased() |
true | Calls super.paintComponent(), erases drag-Item from the list and paints dragImage at the current mouse location. | Updates dragRect’s position, updates the ListModel and calls repaint() when needed. | Sets inDrag to false, calls repaint() and notifies Reorder-Listeners. |
false | Calls super.paintComponent(). | Calls createDragImage if it determines that inDrag should become true. | Does nothing. |
Two methods handle the actual painting of the DragList. These are: createDragImage() which is called by mouseDragged() when mouseDragged() has just set inDrag to true; and paintComponent() which is called by the Swing painting mechanism when mouseDragged() and mouseReleased() call repaint().
When called, createDragImage() creates an image of dragItem and paints it into dragImage. It does this by obtaining the component that would be used to “rubber stamp” dragItem’s image onto the list and using a handy SwingUtilities method to paint this component into dragImage. If dragImage is null or not the right size, createDragImage() first initializes dragImage to a new image of the correct size.
The paintComponent() method is called by the Swing painting mechanism in response to calls to repaint(). If inDrag is true, paintComponent() erases the area where dragItem was painted by filling dragItem’s rectangular area with the DragList’s background color. Then it sets the graphics context to 50% transparency and paints dragImage at the location of dragRect.
Changes to MainFrame are straightforward. MainFrame implements DragList.ReorderListener, codes listReordered(DragList.ReorderEvent) to call redrawBoy() and declares the garmentList attribute to be a DragList instead of a JList. Just because this is the final step, its constructor also creates a slightly fancier interface.
Example 14.15: chap14.gui5.MainFrame.java
1 package chap14.gui5; 2 3 import java.awt.BorderLayout; 4 import java.awt.Container; 5 import java.awt.Image; 6 import java.awt.Point; 7 import java.util.Collections; 8 import java.util.Vector; 9 10 import javax.swing.BorderFactory; 11 import javax.swing.JFrame; 12 import javax.swing.JLabel; 13 import javax.swing.JList; 14 import javax.swing.JPanel; 15 import javax.swing.ListModel; 16 import javax.swing.border.BevelBorder; 17 18 import utils.ResourceUtils; 19 import chap14.gui0.DressingBoard; 20 import chap14.gui0.Garment; 21 import chap14.gui4.CheckboxListCell; 22 23 public class MainFrame 24 extends JFrame 25 implements DragList.ReorderListener, CheckboxListCell.ToggleListener { 26 27 private DressingBoard dressingBoard; 28 private DragList garmentList; 29 30 public MainFrame() { 31 setTitle(getClass().getName()); 32 setSize(600, 400); 33 setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE); 34 35 JPanel topPanel = new JPanel(new BorderLayout()); 36 topPanel.setBorder(BorderFactory.createEtchedBorder()); 37 38 Container contentPane = getContentPane(); 39 contentPane.setLayout(new BorderLayout()); 40 contentPane.add("Center", topPanel); 41 42 JPanel mainPanel = new JPanel(new BorderLayout()); 43 topPanel.add("Center", mainPanel); 44 String instructions = 45 "Click the checkboxes to put on or remove garments." 46 + " Drag the labels to determine order."; 47 topPanel.add("North", new JLabel(instructions, JLabel.CENTER)); 48 49 dressingBoard = new DressingBoard(); 50 mainPanel.add("Center", dressingBoard); 51 52 garmentList = new DragList(); 53 garmentList.setBorder(BorderFactory.createBevelBorder(BevelBorder.LOWERED)); 54 CheckboxListCell ccr = new CheckboxListCell() { 55 protected boolean getCheckedValue(Object value) { 56 return ((Garment)value).isWorn(); 57 } 58 }; 59 ccr.addToggleListener(this); 60 garmentList.addMouseListener(ccr); 61 garmentList.setCellRenderer(ccr); 62 garmentList.addReorderListener(this); 63 mainPanel.add("West", garmentList); 64 65 initList(); 66 } 67 private void initList() { 68 String[] names = 69 { 70 "T-Shirt", 71 "Briefs", 72 "Left Sock", 73 "Right Sock", 74 "Shirt", 75 "Pants", 76 "Belt", 77 "Tie", 78 "Left Shoe", 79 "Right Shoe" }; 80 Point[] points = 81 { 82 new Point(75, 125), 83 new Point(86, 197), 84 new Point(127, 256), 85 new Point(45, 260), 86 new Point(69, 118), 87 new Point(82, 199), 88 new Point(88, 203), 89 new Point(84, 124), 90 new Point(129, 258), 91 new Point(40, 268)}; 92 93 Vector garments = new Vector(names.length); 94 for (int i = 0; i < names.length; ++i) { 95 Image image = 96 ResourceUtils.loadImage("chap14/images/" + names[i] + ".gif", this); 97 Garment garment = new Garment(image, points[i].x, points[i].y, names[i]); 98 garments.add(garment); 99 } 100 Collections.shuffle(garments); 101 garmentList.setListData(garments); 102 } 103 public void listReordered(DragList.ReorderEvent e) { 104 redrawBoy(); 105 } 106 public void checkboxToggled(CheckboxListCell.ToggleEvent event) { 107 JList list = (JList)event.getSource(); 108 int index = event.getIndex(); 109 Garment garment = (Garment)list.getModel().getElementAt(index); 110 garment.setWorn(!garment.isWorn()); 111 redrawBoy(); 112 } 113 private void redrawBoy() { 114 ListModel lm = garmentList.getModel(); 115 int stop = lm.getSize(); 116 Vector order = new Vector(); 117 for (int i = 0; i < stop; ++i) { 118 Garment garment = (Garment)lm.getElementAt(i); 119 if (garment.isWorn()) { 120 order.add(garment); 121 } 122 } 123 dressingBoard.setOrder((Garment[])order.toArray(new Garment[0])); 124 } 125 public static void main(String[] arg) { 126 new MainFrame().setVisible(true); 127 } 128 }
Use the following commands to compile and execute the example. From the directory containing the src folder:
javac –d classes -sourcepath src src/chap14/gui5/MainFrame.java java –cp classes chap14.gui5.MainFrame
We’re done! Run the program. Have fun. In the process of creating this application you have gained experience with several advanced features of the Swing API. This is just the tip of the iceberg, but I hope it gives you a good feeling for the power Swing offers that is just waiting to be harnessed by a creative and determined mind.