1 /*
2  * Copyright (c) 2001, 2002, Oracle and/or its affiliates. All rights reserved.
3  * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
4  *
5  * This code is free software; you can redistribute it and/or modify it
6  * under the terms of the GNU General Public License version 2 only, as
7  * published by the Free Software Foundation.
8  *
9  * This code is distributed in the hope that it will be useful, but WITHOUT
10  * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
11  * FITNESS FOR A PARTICULAR PURPOSE.  See the GNU General Public License
12  * version 2 for more details (a copy is included in the LICENSE file that
13  * accompanied this code).
14  *
15  * You should have received a copy of the GNU General Public License version
16  * 2 along with this work; if not, write to the Free Software Foundation,
17  * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA.
18  *
19  * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA
20  * or visit www.oracle.com if you need additional information or have any
21  * questions.
22  *
23  */
24 
25 package sun.jvm.hotspot.ui;
26 
27 import java.awt.*;
28 import java.awt.datatransfer.*;
29 import java.awt.event.*;
30 import java.io.IOException;
31 import java.math.*;
32 import java.util.*;
33 import javax.swing.*;
34 import javax.swing.event.*;
35 import javax.swing.table.*;
36 import sun.jvm.hotspot.debugger.*;
37 import sun.jvm.hotspot.ui.*;
38 
39 public class MemoryPanel extends JPanel {
40   private boolean is64Bit;
41   private Debugger debugger;
42   private int addressSize;
43   private String unmappedAddrString;
44   private HighPrecisionJScrollBar scrollBar;
45   private AbstractTableModel model;
46   private JTable table;
47   private BigInteger startVal;
48   // Includes any partially-visible row at the bottom
49   private int numVisibleRows;
50   // Frequently-used subexpression
51   private int numUsableRows;
52   // Multi-row (and multi-column) selection. Have to duplicate state
53   // from UI so this can work as we scroll off the screen.
54   private boolean haveAnchor;
55   private int     rowAnchorIndex;
56   private int     colAnchorIndex;
57   private boolean haveLead;
58   private int     rowLeadIndex;
59   private int     colLeadIndex;
60 
61   abstract class ActionWrapper extends AbstractAction {
62     private Action parent;
ActionWrapper()63     ActionWrapper() {
64     }
65 
setParent(Action parent)66     void setParent(Action parent) {
67       this.parent = parent;
68     }
69 
getParent()70     Action getParent() {
71       return parent;
72     }
73 
actionPerformed(ActionEvent e)74     public void actionPerformed(ActionEvent e) {
75       if (getParent() != null) {
76         getParent().actionPerformed(e);
77       }
78     }
79   }
80 
MemoryPanel(final Debugger debugger, boolean is64Bit)81   public MemoryPanel(final Debugger debugger, boolean is64Bit) {
82     super();
83     this.debugger = debugger;
84     this.is64Bit = is64Bit;
85     if (is64Bit) {
86       addressSize = 8;
87       unmappedAddrString = "??????????????????";
88     } else {
89       addressSize = 4;
90       unmappedAddrString = "??????????";
91     }
92     setLayout(new BorderLayout());
93     setupScrollBar();
94     add(scrollBar, BorderLayout.EAST);
95 
96     model = new AbstractTableModel() {
97         public int getRowCount() {
98           return numVisibleRows;
99         }
100         public int getColumnCount() {
101           return 2;
102         }
103         public Object getValueAt(int row, int column) {
104           switch (column) {
105           case 0:  return bigIntToHexString(startVal.add(new BigInteger(Integer.toString((row * addressSize)))));
106           case 1: {
107             try {
108               Address addr = bigIntToAddress(startVal.add(new BigInteger(Integer.toString((row * addressSize)))));
109               if (addr != null) {
110                 return addressToString(addr.getAddressAt(0));
111               }
112               return unmappedAddrString;
113             } catch (UnmappedAddressException e) {
114               return unmappedAddrString;
115             }
116           }
117           default: throw new RuntimeException("Column " + column + " out of bounds");
118           }
119         }
120         public boolean isCellEditable(int row, int col) {
121           return false;
122         }
123       };
124 
125     // View with JTable with no header
126     table = new JTable(model);
127     table.setTableHeader(null);
128     table.setShowGrid(false);
129     table.setIntercellSpacing(new Dimension(0, 0));
130     table.setCellSelectionEnabled(true);
131     table.setSelectionMode(ListSelectionModel.SINGLE_INTERVAL_SELECTION);
132     table.setDragEnabled(true);
133     Font font = GraphicsUtilities.lookupFont("Courier");
134     if (font == null) {
135       throw new RuntimeException("Error looking up monospace font Courier");
136     }
137     table.setFont(font);
138 
139     // Export proper data.
140     // We need to keep our own notion of the selection in order to
141     // properly export data, since the selection can go beyond the
142     // visible area on the screen (and since the table's model doesn't
143     // back all of those slots).
144     // Code thanks to Shannon.Hickey@sfbay
145     table.setTransferHandler(new TransferHandler() {
146         protected Transferable createTransferable(JComponent c) {
147           JTable table = (JTable)c;
148           if (haveSelection()) {
149             StringBuffer buf = new StringBuffer();
150             int iDir = (getRowAnchor() < getRowLead() ? 1 : -1);
151             int jDir = (getColAnchor() < getColLead() ? 1 : -1);
152 
153             for (int i = getRowAnchor(); i != getRowLead() + iDir; i += iDir) {
154               for (int j = getColAnchor(); j != getColLead() + jDir; j += jDir) {
155                 Object val = model.getValueAt(i, j);
156                 buf.append(val == null ? "" : val.toString());
157                 if (j != getColLead()) {
158                   buf.append("\t");
159                 }
160               }
161               if (i != getRowLead()) {
162                 buf.append("\n");
163               }
164             }
165 
166             return new StringTransferable(buf.toString());
167           }
168           return null;
169         }
170 
171         public int getSourceActions(JComponent c) {
172           return COPY;
173         }
174 
175         public boolean importData(JComponent c, Transferable t) {
176           if (canImport(c, t.getTransferDataFlavors())) {
177             try {
178               String str = (String)t.getTransferData(DataFlavor.stringFlavor);
179               handleImport(c, str);
180               return true;
181             } catch (UnsupportedFlavorException ufe) {
182             } catch (IOException ioe) {
183             }
184           }
185 
186           return false;
187         }
188 
189         public boolean canImport(JComponent c, DataFlavor[] flavors) {
190           for (int i = 0; i < flavors.length; i++) {
191             if (DataFlavor.stringFlavor.equals(flavors[i])) {
192               return true;
193             }
194           }
195           return false;
196         }
197 
198         private void handleImport(JComponent c, String str) {
199           // do whatever you want with the string here
200           try {
201             makeVisible(debugger.parseAddress(str));
202             clearSelection();
203             table.clearSelection();
204           } catch (NumberFormatException e) {
205             System.err.println("Unable to parse address \"" + str + "\"");
206           }
207         }
208       });
209 
210     // Supporting keyboard scrolling
211     // See src/share/classes/javax/swing/plaf/metal/MetalLookAndFeel.java,
212     // search for Table.AncestorInputMap
213 
214     // Actions to override:
215     // selectPreviousRow, selectNextRow,
216     // scrollUpChangeSelection, scrollDownChangeSelection,
217     // selectPreviousRowExtendSelection, selectNextRowExtendSelection,
218     // scrollDownExtendSelection, scrollUpExtendSelection (Shift-PgDn/PgUp)
219 
220     ActionMap map = table.getActionMap();
221 
222     // Up arrow
223     installActionWrapper(map, "selectPreviousRow", new ActionWrapper() {
224         public void actionPerformed(ActionEvent e) {
225           beginUpdate();
226           clearSelection();
227           if (table.getSelectedRow() == 0) {
228             scrollBar.scrollUpOrLeft();
229             table.setRowSelectionInterval(0, 0);
230           } else {
231             super.actionPerformed(e);
232           }
233           maybeGrabSelection();
234           endUpdate();
235         }
236       });
237     // Down arrow
238     installActionWrapper(map, "selectNextRow", new ActionWrapper() {
239         public void actionPerformed(ActionEvent e) {
240           beginUpdate();
241           clearSelection();
242           int row = table.getSelectedRow();
243           if (row >= numUsableRows) {
244             scrollBar.scrollDownOrRight();
245             table.setRowSelectionInterval(row, row);
246           } else {
247             super.actionPerformed(e);
248           }
249           maybeGrabSelection();
250           endUpdate();
251         }
252       });
253     // Page up
254     installActionWrapper(map, "scrollUpChangeSelection", new ActionWrapper() {
255         public void actionPerformed(ActionEvent e) {
256           beginUpdate();
257           clearSelection();
258           int row = table.getSelectedRow();
259           scrollBar.pageUpOrLeft();
260           if (row >= 0) {
261             table.setRowSelectionInterval(row, row);
262           }
263           maybeGrabSelection();
264           endUpdate();
265         }
266       });
267     // Page down
268     installActionWrapper(map, "scrollDownChangeSelection", new ActionWrapper() {
269         public void actionPerformed(ActionEvent e) {
270           beginUpdate();
271           clearSelection();
272           int row = table.getSelectedRow();
273           scrollBar.pageDownOrRight();
274           if (row >= 0) {
275             table.setRowSelectionInterval(row, row);
276           }
277           maybeGrabSelection();
278           endUpdate();
279         }
280       });
281     // Shift + Up arrow
282     installActionWrapper(map, "selectPreviousRowExtendSelection", new ActionWrapper() {
283         public void actionPerformed(ActionEvent e) {
284           beginUpdate();
285           if (!haveAnchor()) {
286             setAnchorFromTable();
287             setLeadFromTable();
288             //            setAnchor(table.getSelectedRow());
289             //            setLead(table.getSelectedRow());
290           }
291           int newLead = getRowLead() - 1;
292           int newAnchor = getRowAnchor();
293           if (newLead < 0) {
294             scrollBar.scrollUpOrLeft();
295             ++newLead;
296             ++newAnchor;
297           }
298           setSelection(newAnchor, newLead, getColAnchor(), getColLead());
299           //          printSelection();
300           endUpdate();
301         }
302       });
303     // Shift + Left arrow
304     installActionWrapper(map, "selectPreviousColumnExtendSelection", new ActionWrapper() {
305         public void actionPerformed(ActionEvent e) {
306           beginUpdate();
307           if (!haveAnchor()) {
308             setAnchorFromTable();
309             setLeadFromTable();
310           }
311           int newLead = Math.max(0, getColLead() - 1);
312           setSelection(getRowAnchor(), getRowLead(), getColAnchor(), newLead);
313           //          printSelection();
314           endUpdate();
315         }
316       });
317     // Shift + Down arrow
318     installActionWrapper(map, "selectNextRowExtendSelection", new ActionWrapper() {
319         public void actionPerformed(ActionEvent e) {
320           beginUpdate();
321           if (!haveAnchor()) {
322             setAnchorFromTable();
323             setLeadFromTable();
324             //            setAnchor(table.getSelectedRow());
325             //            setLead(table.getSelectedRow());
326           }
327           int newLead = getRowLead() + 1;
328           int newAnchor = getRowAnchor();
329           if (newLead > numUsableRows) {
330             scrollBar.scrollDownOrRight();
331             --newLead;
332             --newAnchor;
333           }
334           setSelection(newAnchor, newLead, getColAnchor(), getColLead());
335           //          printSelection();
336           endUpdate();
337         }
338       });
339     // Shift + Right arrow
340     installActionWrapper(map, "selectNextColumnExtendSelection", new ActionWrapper() {
341         public void actionPerformed(ActionEvent e) {
342           beginUpdate();
343           if (!haveAnchor()) {
344             setAnchorFromTable();
345             setLeadFromTable();
346           }
347           int newLead = Math.min(model.getColumnCount() - 1, getColLead() + 1);
348           setSelection(getRowAnchor(), getRowLead(), getColAnchor(), newLead);
349           //          printSelection();
350           endUpdate();
351         }
352       });
353     // Shift + Page up
354     installActionWrapper(map, "scrollUpExtendSelection", new ActionWrapper() {
355         public void actionPerformed(ActionEvent e) {
356           beginUpdate();
357           if (!haveAnchor()) {
358             setAnchorFromTable();
359             setLeadFromTable();
360             //            setAnchor(table.getSelectedRow());
361             //            setLead(table.getSelectedRow());
362           }
363           int newLead = getRowLead() - numUsableRows;
364           int newAnchor = getRowAnchor();
365           if (newLead < 0) {
366             scrollBar.pageUpOrLeft();
367             newLead += numUsableRows;
368             newAnchor += numUsableRows;
369           }
370           setSelection(newAnchor, newLead, getColAnchor(), getColLead());
371           //          printSelection();
372           endUpdate();
373         }
374       });
375     // Shift + Page down
376     installActionWrapper(map, "scrollDownExtendSelection", new ActionWrapper() {
377         public void actionPerformed(ActionEvent e) {
378           beginUpdate();
379           if (!haveAnchor()) {
380             setAnchorFromTable();
381             setLeadFromTable();
382             //            setAnchor(table.getSelectedRow());
383             //            setLead(table.getSelectedRow());
384           }
385           int newLead = getRowLead() + numUsableRows;
386           int newAnchor = getRowAnchor();
387           if (newLead > numUsableRows) {
388             scrollBar.pageDownOrRight();
389             newLead -= numUsableRows;
390             newAnchor -= numUsableRows;
391           }
392           setSelection(newAnchor, newLead, getColAnchor(), getColLead());
393           //          printSelection();
394           endUpdate();
395         }
396       });
397 
398     // Clear our notion of selection upon mouse press
399     table.addMouseListener(new MouseAdapter() {
400         public void mousePressed(MouseEvent e) {
401           if (shouldIgnore(e)) {
402             return;
403           }
404           // Make shift-clicking work properly
405           if (e.isShiftDown()) {
406             maybeGrabSelection();
407             return;
408           }
409           //          System.err.println("  Clearing selection on mouse press");
410           clearSelection();
411         }
412       });
413 
414     // Watch for mouse going out of bounds
415     table.addMouseMotionListener(new MouseMotionAdapter() {
416         public void mouseDragged(MouseEvent e) {
417           if (shouldIgnore(e)) {
418             //            System.err.println("  (Ignoring consumed mouse event)");
419             return;
420           }
421 
422           // Look for drag events outside table and scroll if necessary
423           Point p = e.getPoint();
424           if (table.rowAtPoint(p) == -1) {
425             // See whether we are above or below the table
426             Rectangle rect = new Rectangle();
427             getBounds(rect);
428             beginUpdate();
429             if (p.y < rect.y) {
430               //              System.err.println("  Scrolling up due to mouse event");
431               // Scroll up
432               scrollBar.scrollUpOrLeft();
433               setSelection(getRowAnchor(), 0, getColAnchor(), getColLead());
434             } else {
435               //              System.err.println("  Scrolling down due to mouse event");
436               // Scroll down
437               scrollBar.scrollDownOrRight();
438               setSelection(getRowAnchor(), numUsableRows, getColAnchor(), getColLead());
439             }
440             //            printSelection();
441             endUpdate();
442           } else {
443             maybeGrabSelection();
444           }
445         }
446       });
447 
448 
449     add(table, BorderLayout.CENTER);
450 
451     // Make sure we recompute number of visible rows
452     addComponentListener(new ComponentAdapter() {
453         public void componentResized(ComponentEvent e) {
454           recomputeNumVisibleRows();
455           constrain();
456         }
457       });
458     addHierarchyListener(new HierarchyListener() {
459         public void hierarchyChanged(HierarchyEvent e) {
460           recomputeNumVisibleRows();
461           constrain();
462         }
463       });
464     updateFromScrollBar();
465   }
466 
467   /** Makes the given address visible somewhere in the window */
makeVisible(Address addr)468   public void makeVisible(Address addr) {
469     BigInteger bi = addressToBigInt(addr);
470     scrollBar.setValueHP(bi);
471   }
472 
473   //----------------------------------------------------------------------
474   // Internals only below this point
475   //
476 
setupScrollBar()477   private void setupScrollBar() {
478     if (is64Bit) {
479       // 64-bit mode
480       scrollBar =
481         new HighPrecisionJScrollBar(
482           Scrollbar.VERTICAL,
483           new BigInteger(1, new byte[] {
484             (byte) 0x80, (byte) 0x00, (byte) 0x00, (byte) 0x00,
485             (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00}),
486           new BigInteger(1, new byte[] {
487             (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00,
488             (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00}),
489           new BigInteger(1, new byte[] {
490             (byte) 0xFF, (byte) 0xFF, (byte) 0xFF, (byte) 0xFF,
491             (byte) 0xFF, (byte) 0xFF, (byte) 0xFF, (byte) 0xFC}));
492       scrollBar.setUnitIncrementHP(new BigInteger(1, new byte[] {
493         (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00,
494         (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x08}));
495       scrollBar.setBlockIncrementHP(new BigInteger(1, new byte[] {
496         (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00,
497         (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x40}));
498     } else {
499       // 32-bit mode
500       scrollBar=
501         new HighPrecisionJScrollBar(
502           Scrollbar.VERTICAL,
503           new BigInteger(1, new byte[] {
504             (byte) 0x80, (byte) 0x00, (byte) 0x00, (byte) 0x00}),
505           new BigInteger(1, new byte[] {
506             (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00}),
507           new BigInteger(1, new byte[] {
508             (byte) 0xFF, (byte) 0xFF, (byte) 0xFF, (byte) 0xFC}));
509       scrollBar.setUnitIncrementHP(new BigInteger(1, new byte[] {
510         (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x04}));
511       scrollBar.setBlockIncrementHP(new BigInteger(1, new byte[] {
512         (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x20}));
513     }
514     scrollBar.addChangeListener(new ChangeListener() {
515         public void stateChanged(ChangeEvent e) {
516           updateFromScrollBar();
517         }
518       });
519   }
520 
updateFromScrollBar()521   private void updateFromScrollBar() {
522     beginUpdate();
523     BigInteger oldStartVal = startVal;
524     startVal = scrollBar.getValueHP();
525     constrain();
526     model.fireTableDataChanged();
527     if (oldStartVal != null) {
528       modifySelection(oldStartVal.subtract(startVal).intValue() / addressSize);
529     }
530     endUpdate();
531   }
532 
constrain()533   private void constrain() {
534     BigInteger offset = new BigInteger(Integer.toString(addressSize * (numUsableRows)));
535     BigInteger endVal = startVal.add(offset);
536     if (endVal.compareTo(scrollBar.getMaximumHP()) > 0) {
537       startVal = scrollBar.getMaximumHP().subtract(offset);
538       endVal   = scrollBar.getMaximumHP();
539       scrollBar.setValueHP(startVal);
540       model.fireTableDataChanged();
541     }
542   }
543 
recomputeNumVisibleRows()544   private void recomputeNumVisibleRows() {
545     Rectangle rect = new Rectangle();
546     getBounds(rect);
547     int h = table.getRowHeight();
548     numVisibleRows = (rect.height + (h - 1)) / h;
549     numUsableRows  = numVisibleRows - 2;
550     scrollBar.setBlockIncrementHP(new BigInteger(Integer.toString(addressSize * (numUsableRows))));
551     model.fireTableDataChanged();
552     // FIXME: refresh selection
553   }
554 
bigIntToHexString(BigInteger bi)555   private String bigIntToHexString(BigInteger bi) {
556     StringBuffer buf = new StringBuffer();
557     buf.append("0x");
558     String val = bi.toString(16);
559     for (int i = 0; i < ((2 * addressSize) - val.length()); i++) {
560       buf.append('0');
561     }
562     buf.append(val);
563     return buf.toString();
564   }
565 
bigIntToAddress(BigInteger i)566   private Address bigIntToAddress(BigInteger i) {
567     String s = bigIntToHexString(i);
568     return debugger.parseAddress(s);
569   }
570 
addressToBigInt(Address a)571   private BigInteger addressToBigInt(Address a) {
572     String s = addressToString(a);
573     if (!s.startsWith("0x")) {
574       throw new NumberFormatException(s);
575     }
576     return new BigInteger(s.substring(2), 16);
577   }
578 
addressToString(Address a)579   private String addressToString(Address a) {
580     if (a == null) {
581       if (is64Bit) {
582         return "0x0000000000000000";
583       } else {
584         return "0x00000000";
585       }
586     }
587     return a.toString();
588   }
589 
installActionWrapper(ActionMap map, String actionName, ActionWrapper wrapper)590   private static void installActionWrapper(ActionMap map,
591                                            String actionName,
592                                            ActionWrapper wrapper) {
593     wrapper.setParent(map.get(actionName));
594     map.put(actionName, wrapper);
595   }
596 
shouldIgnore(MouseEvent e)597   private boolean shouldIgnore(MouseEvent e) {
598     return e.isConsumed() || (!(SwingUtilities.isLeftMouseButton(e) && table.isEnabled()));
599   }
600 
clearSelection()601   private void clearSelection() {
602     haveAnchor = false;
603     haveLead = false;
604   }
605 
606   private int updateLevel;
updating()607   private boolean updating()         { return updateLevel > 0; }
beginUpdate()608   private void    beginUpdate()      { ++updateLevel;          }
endUpdate()609   private void    endUpdate()        { --updateLevel;          }
610 
haveAnchor()611   private boolean haveAnchor()       { return haveAnchor;  }
haveLead()612   private boolean haveLead()         { return haveLead;    }
haveSelection()613   private boolean haveSelection()    { return haveAnchor() && haveLead(); }
getRowAnchor()614   private int     getRowAnchor()     { return rowAnchorIndex; }
getColAnchor()615   private int     getColAnchor()     { return colAnchorIndex; }
getRowLead()616   private int     getRowLead()       { return rowLeadIndex;   }
getColLead()617   private int     getColLead()       { return colLeadIndex;   }
618 
setAnchorFromTable()619   private void setAnchorFromTable() {
620     setAnchor(table.getSelectionModel().getAnchorSelectionIndex(),
621               table.getColumnModel().getSelectionModel().getAnchorSelectionIndex());
622   }
setLeadFromTable()623   private void setLeadFromTable() {
624     setLead(table.getSelectionModel().getAnchorSelectionIndex(),
625             table.getColumnModel().getSelectionModel().getAnchorSelectionIndex());
626   }
setAnchor(int row, int col)627   private void setAnchor(int row, int col) {
628     rowAnchorIndex = row;
629     colAnchorIndex = col;
630     haveAnchor = true;
631   }
setLead(int row, int col)632   private void setLead(int row, int col) {
633     rowLeadIndex = row;
634     colLeadIndex = col;
635     haveLead = true;
636   }
clamp(int val, int min, int max)637   private int clamp(int val, int min, int max) {
638     return Math.max(Math.min(val, max), min);
639   }
maybeGrabSelection()640   private void maybeGrabSelection() {
641     if (table.getSelectedRow() != -1) {
642       // Grab selection
643       ListSelectionModel rowSel = table.getSelectionModel();
644       ListSelectionModel colSel = table.getColumnModel().getSelectionModel();
645       if (!haveAnchor()) {
646         //        System.err.println("Updating from table's selection");
647         setSelection(rowSel.getAnchorSelectionIndex(), rowSel.getLeadSelectionIndex(),
648                      colSel.getAnchorSelectionIndex(), colSel.getLeadSelectionIndex());
649       } else {
650         //        System.err.println("Updating lead from table's selection");
651         setSelection(getRowAnchor(), rowSel.getLeadSelectionIndex(),
652                      getColAnchor(), colSel.getLeadSelectionIndex());
653       }
654       //      printSelection();
655     }
656   }
setSelection(int rowAnchor, int rowLead, int colAnchor, int colLead)657   private void setSelection(int rowAnchor, int rowLead, int colAnchor, int colLead) {
658     setAnchor(rowAnchor, colAnchor);
659     setLead(rowLead, colLead);
660     table.setRowSelectionInterval(clamp(rowAnchor, 0, numUsableRows),
661                                   clamp(rowLead, 0, numUsableRows));
662     table.setColumnSelectionInterval(colAnchor, colLead);
663   }
modifySelection(int amount)664   private void modifySelection(int amount) {
665     if (haveSelection()) {
666       setSelection(getRowAnchor() + amount, getRowLead() + amount,
667                    getColAnchor(), getColLead());
668     }
669   }
printSelection()670   private void printSelection() {
671     System.err.println("Selection updated to (" +
672                        model.getValueAt(getRowAnchor(), getColAnchor()) +
673                        ", " +
674                        model.getValueAt(getRowLead(), getColLead()) + ") [(" +
675                        getRowAnchor() + ", " + getColAnchor() + "), (" +
676                        getRowLead() + ", " + getColLead() + ")]");
677   }
678 }
679