1 /*
2  * @(#)QuaquaTableUI.java  1.10.1  2008-11-05
3  *
4  * Copyright (c) 2004-2008 Werner Randelshofer
5  * Staldenmattweg 2, Immensee, CH-6405, Switzerland.
6  * All rights reserved.
7  *
8  * The copyright of this software is owned by Werner Randelshofer.
9  * You may not use, copy or modify this software, except in
10  * accordance with the license agreement you entered into with
11  * Werner Randelshofer. For details see accompanying license terms.
12  */
13 package ch.randelshofer.quaqua;
14 
15 import ch.randelshofer.quaqua.color.InactivatableColorUIResource;
16 import ch.randelshofer.quaqua.util.ViewportPainter;
17 import java.awt.*;
18 import java.awt.datatransfer.*;
19 import java.awt.dnd.*;
20 import java.awt.event.*;
21 import java.awt.image.*;
22 import java.beans.*;
23 import java.util.*;
24 import javax.swing.*;
25 import javax.swing.event.*;
26 import javax.swing.plaf.*;
27 import javax.swing.plaf.basic.*;
28 import javax.swing.table.*;
29 import javax.swing.event.*;
30 import javax.swing.text.*;
31 
32 /**
33  * QuaquaTableUI.
34  *
35  * @author  Werner Randelshofer
36  * @version 1.10.1 2008-11-05 Included fix by laurie_int regarding performance
37  * issue #72 "Table renders slow with very large tables".
38  * <br>1.10 2008-07-15 System property "Quaqua.Table.useJ2SE5MouseHandler"
39  * enforces use of J2SE5 mouse handler for J2SE6.
40  * <br>1.9.3 2008-07-06 Java 1.4 incorrectly reports button 3 pressed when
41  * the user presses the meta key.
42  * <br>1.9.2 2008-06-22 Selection foreground color must be set to
43  * inactive (=black) when the current cell is not selected, otherwise we get
44  * white text on white background.
45  * <br>1.9.1 2008-05-31 Moved all code related to InactivateableColorUIResource
46  * into method paintCell.
47  * <br>1.9 2008-05-10 Treat table as focused, if it is focused or if
48  * it is editing a table cell.
49  * <br>1.8.1 2008-05-03 Multiple cell selection did not work.
50  * <br>1.8 2008-04-21 Set client property "terminateEditOnFocusLost" to
51  * true on initDefaults. On mousePressed, requestFocusInWindow.
52  * <br>1.7 2008-03-21 Made selection behavior more consistent with native
53  * NSTable control.
54  * <br>1.6 2008-02-07 Reworked drawing of list selection. Implemented
55  * ListSelectionListener to ensure that selection changes are properly repainted.
56  * <br>1.5 2008-01-13 Set 'showHorizontalLines' and 'showVerticalLines' to
57  * false once when installing the UI, instead of overwriting these properties every
58  * time when the client property "Quaqua.Table.style" is changed.
59  * <br>1.4 2007-01-16 Focus border repainting factored out into QuaquaViewportUI.
60  * <br>1.3.3 2007-01-15 Change foreground color of cell renderer even if
61  * it is not an UIResource.
62  * <br>1.3.2 2007-01-05 Issue #6: Selection needs to be drawn differently
63  * when table hasn't focus or is disabled or is on an inactive window.
64  * Issue #10: Table cells mustn't draw selection background when
65  * rowSelectionAllowed is false.
66  * <br>1.3.1 2006-05-04 EditorCell was always drawn with alternating
67  * row2 color even when the table style was not set to striped.
68  * <br>1.3 2006-02-07 Support for client property "Table.isFileList" added.
69  * <br>1.2.1 2005-08-25 If the table is not striped, fill the viewport with
70  * the background color of the table.
71  * <br>1.2 2005-03-11 LnF Property "Table.alternateBackground" replaced
72  * by "Table.alternateBackground.0" and "Table.alternateBackground.1".
73  * <br>1.1 2004-07-04 FocusHandler added.
74  * <br>1.0  June 22, 2004  Created.
75  */
76 public class QuaquaTableUI extends BasicTableUI
77         implements ViewportPainter {
78 
79     private PropertyChangeListener propertyChangeListener;
80     private ListSelectionListener listSelectionListener;
81     private TableColumnModelListener columnModelListener;
82     private Handler handler;
83     private boolean isStriped = false;
84 
85     /** Creates a new instance. */
QuaquaTableUI()86     public QuaquaTableUI() {
87     }
88 
createUI(JComponent c)89     public static ComponentUI createUI(JComponent c) {
90         return new QuaquaTableUI();
91     }
92 
93     /**
94      * Creates the key listener for handling keyboard navigation in the JTable.
95      */
createKeyListener()96     protected KeyListener createKeyListener() {
97         return new KeyAdapter() {
98 
99             public void keyPressed(KeyEvent e) {
100                 // Eat away META down keys..
101                 // We need to do this, because the JTable.processKeyBinding(…)
102                 // method does not treat VK_META as a modifier key, and starts
103                 // editing a cell, whenever this key is pressed.
104                 if (e.getKeyCode() == KeyEvent.VK_META) {
105                     e.consume();
106                 }
107             }
108         };
109     }
110 
111     private Color getAlternateColor(int modulo) {
112         if (modulo == 0) {
113             return UIManager.getColor("Table.alternateBackground.0");
114         } else {
115             return UIManager.getColor("Table.alternateBackground.1");
116         }
117     }
118 
119     /**
120      * Attaches listeners to the JTable.
121      */
122     protected void installListeners() {
123         super.installListeners();
124         propertyChangeListener = createPropertyChangeListener();
125         table.addPropertyChangeListener(propertyChangeListener);
126         listSelectionListener = createListSelectionListener();
127         if (table.getSelectionModel() != null) {
128             table.getSelectionModel().addListSelectionListener(listSelectionListener);
129         }
130         columnModelListener = createTableColumnModelListener();
131         if (table.getColumnModel() != null) {
132             table.getColumnModel().addColumnModelListener(columnModelListener);
133         }
134     // table.add
135     }
136 
137     protected void uninstallListeners() {
138         super.uninstallListeners();
139         table.removePropertyChangeListener(propertyChangeListener);
140         if (table.getSelectionModel() != null) {
141             table.getSelectionModel().removeListSelectionListener(listSelectionListener);
142         }
143         if (table.getColumnModel() != null) {
144             table.getColumnModel().removeColumnModelListener(columnModelListener);
145         }
146         propertyChangeListener = null;
147         listSelectionListener = null;
148 
149     }
150 
151     protected void installDefaults() {
152         super.installDefaults();
153         Object property = table.getClientProperty("Quaqua.Table.style");
154         isStriped = property != null && property.equals("striped");
155         updateStriped();
156         table.setShowHorizontalLines(false);
157         table.setShowVerticalLines(false);
158         // table.putClientProperty("JTable.autoStartsEdit", Boolean.FALSE);
159 
160         // By default, terminate editing on focus lost.
161         table.putClientProperty("terminateEditOnFocusLost", Boolean.TRUE);
162 
163     // FIXME - Intercell spacings different from 1,1 don't work currently
164     //table.setIntercellSpacing(new Dimension(4,4));
165     }
166 
167     private void updateStriped() {
168         /*if (isStriped) {
169         table.setIntercellSpacing(new Dimension(1, 1));
170         } else {
171         //getTableHeader().setDefaultRenderer(new DefaultTableHeaderRenderer());
172         table.setIntercellSpacing(new Dimension(1, 1));
173         }*/
174     }
175 
176     /** Paint a representation of the <code>table</code> instance
177      * that was set in installUI().
178      */
179     public void paint(Graphics g, JComponent c) {
180         if (table.getRowCount() <= 0 || table.getColumnCount() <= 0) {
181             return;
182         }
183         Rectangle clip = g.getClipBounds();
184         Point upperLeft = clip.getLocation();
185         Point lowerRight = new Point(clip.x + clip.width - 1, clip.y + clip.height - 1);
186         int rMin = table.rowAtPoint(upperLeft);
187         int rMax = table.rowAtPoint(lowerRight);
188         // This should never happen.
189         if (rMin == -1) {
190             rMin = 0;
191         }
192         // If the table does not have enough rows to fill the view we'll get -1.
193         // Replace this with the row2 of the last row2.
194         if (rMax == -1) {
195             rMax = table.getRowCount() - 1;
196         }
197 
198         boolean ltr = table.getComponentOrientation().isLeftToRight();
199         int cMin = table.columnAtPoint(ltr ? upperLeft : lowerRight);
200         int cMax = table.columnAtPoint(ltr ? lowerRight : upperLeft);
201         // This should never happen.
202         if (cMin == -1) {
203             cMin = 0;
204         }
205         // If the table does not have enough columns to fill the view we'll get -1.
206         // Replace this with the row2 of the last column.
207         if (cMax == -1) {
208             cMax = table.getColumnCount() - 1;
209         }
210 
211         // Paint the cells.
212         paintCells(g, rMin, rMax, cMin, cMax);
213         // Paint the grid.
214         paintGrid(g, rMin, rMax, cMin, cMax);
215     }
216 
217     public void paintViewport(Graphics g, JViewport c) {
218         Dimension vs = c.getSize();
219         Dimension ts = table.getSize();
220         Point p = table.getLocation();
221         int rh = table.getRowHeight();
222         int n = table.getRowCount();
223         int row = Math.abs(p.y / rh);
224         int th = n * rh - row * rh;
225 
226 
227         if (isStriped) {
228             // Fill the viewport with alternate color 1
229             g.setColor(getAlternateColor(1));
230             g.fillRect(0, 0, c.getWidth(), c.getHeight());
231 
232             // Now check if we need to paint some stripes
233             g.setColor(getAlternateColor(0));
234 
235             // Paint empty rows at the right to fill the viewport
236             if (ts.width < vs.width) {
237                 for (int y = p.y + row * rh, ymax = Math.min(th, vs.height); y < ymax; y+=rh) {
238                     if (row % 2 == 0) {
239                         g.fillRect(0, y, vs.width, rh);
240                     }
241                     row++;
242                 }
243             }
244 
245 
246             // Paint empty rows at the bottom to fill the viewport
247             if (th < vs.height) {
248                 row = n;
249                 int y = th;
250                 while (y < vs.height) {
251                     if (row % 2 == 0) {
252                         g.fillRect(0, y, vs.width, rh);
253                     }
254                     y += rh;
255                     row++;
256                 }
257             }
258         } else {
259             // Fill the viewport with the background color of the table
260             g.setColor(table.getBackground());
261             g.fillRect(0, 0, c.getWidth(), c.getHeight());
262         }
263 
264         // Paint the horizontal grid lines
265         if (table.getShowHorizontalLines()) {
266             g.setColor(table.getGridColor());
267             if (ts.width < vs.width) {
268                 row = Math.abs(p.y / rh);
269                 int y = p.y + row * rh + rh - 1;
270                 while (y < th) {
271                     g.drawLine(0, y, vs.width, y);
272                     y += rh;
273                 }
274             }
275             if (th < vs.height) {
276                 int y = th + rh - 1;
277                 while (y < vs.height) {
278                     g.drawLine(0, y, vs.width, y);
279                     y += rh;
280                 }
281             }
282         }
283 
284 
285         // Paint the vertical grid lines
286         if (th < vs.height && table.getShowVerticalLines()) {
287             g.setColor(table.getGridColor());
288             TableColumnModel cm = table.getColumnModel();
289             n = cm.getColumnCount();
290             int y = th;
291             int x = table.getX() - 1;
292             for (int i = 0; i < n; i++) {
293                 TableColumn col = cm.getColumn(i);
294                 x += col.getWidth();
295                 g.drawLine(x, y, x, vs.height);
296             }
297         }
298     }
299 
300     /*
301      * Paints the grid lines within <I>aRect</I>, using the grid
302      * color set with <I>setGridColor</I>. Paints vertical lines
303      * if <code>getShowVerticalLines()</code> returns true and paints
304      * horizontal lines if <code>getShowHorizontalLines()</code>
305      * returns true.
306      */
307     private void paintGrid(Graphics g, int rMin, int rMax, int cMin, int cMax) {
308         g.setColor(table.getGridColor());
309         Rectangle minCell = table.getCellRect(rMin, cMin, true);
310         Rectangle maxCell = table.getCellRect(rMax, cMax, true);
311         Rectangle damagedArea = minCell.union(maxCell);
312 
313         if (table.getShowHorizontalLines()) {
314             int tableWidth = damagedArea.x + damagedArea.width;
315             int y = damagedArea.y;
316             for (int row = rMin; row <= rMax; row++) {
317                 y += table.getRowHeight(row);
318                 g.drawLine(damagedArea.x, y - 1, tableWidth - 1, y - 1);
319             }
320         }
321         if (table.getShowVerticalLines()) {
322             JTableHeader header = table.getTableHeader();
323             TableColumn draggedColumn = (header == null) ? null : header.getDraggedColumn();
324             Rectangle vacatedColumnRect;
325             if (draggedColumn != null) {
326                 int draggedColumnIndex = viewIndexForColumn(draggedColumn);
327 
328                 Rectangle minDraggedCell = table.getCellRect(rMin, draggedColumnIndex, true);
329                 Rectangle maxDraggedCell = table.getCellRect(rMax, draggedColumnIndex, true);
330 
331                 vacatedColumnRect = minDraggedCell.union(maxDraggedCell);
332 
333                 // Move to the where the cell has been dragged.
334                 vacatedColumnRect.x += header.getDraggedDistance();
335             } else {
336                 vacatedColumnRect = new Rectangle(0, 0, -1, -1);
337             }
338 
339             TableColumnModel cm = table.getColumnModel();
340             int tableHeight = damagedArea.y + damagedArea.height;
341             int x;
342             if (table.getComponentOrientation().isLeftToRight()) {
343                 x = damagedArea.x;
344                 for (int column = cMin; column <= cMax; column++) {
345                     int w = cm.getColumn(column).getWidth();
346                     x += w;
347                     if (x < vacatedColumnRect.x || x > vacatedColumnRect.x + vacatedColumnRect.width) {
348                         g.drawLine(x - 1, 0, x - 1, tableHeight - 1);
349                     }
350                 }
351             } else {
352                 x = damagedArea.x + damagedArea.width;
353                 for (int column = cMin; column < cMax; column++) {
354                     int w = cm.getColumn(column).getWidth();
355                     x -= w;
356                     if (x < vacatedColumnRect.x || x > vacatedColumnRect.x + vacatedColumnRect.width) {
357                         g.drawLine(x - 1, 0, x - 1, tableHeight - 1);
358                     }
359                 }
360                 x -= cm.getColumn(cMax).getWidth();
361                 g.drawLine(x, 0, x, tableHeight - 1);
362             }
363         }
364     }
365 
366     private void paintDraggedArea(Graphics g, int rMin, int rMax, TableColumn draggedColumn, int distance) {
367         int draggedColumnIndex = viewIndexForColumn(draggedColumn);
368 
369         Rectangle minCell = table.getCellRect(rMin, draggedColumnIndex, true);
370         Rectangle maxCell = table.getCellRect(rMax, draggedColumnIndex, true);
371 
372         Rectangle vacatedColumnRect = minCell.union(maxCell);
373 
374         // Paint a gray well in place of the moving column.
375         g.setColor(table.getParent().getBackground());
376         g.fillRect(vacatedColumnRect.x, vacatedColumnRect.y,
377                 vacatedColumnRect.width, vacatedColumnRect.height);
378 
379         // Move to the where the cell has been dragged.
380         vacatedColumnRect.x += distance;
381 
382         // Fill the background.
383         g.setColor(table.getBackground());
384         g.fillRect(vacatedColumnRect.x, vacatedColumnRect.y,
385                 vacatedColumnRect.width, vacatedColumnRect.height);
386 
387         // Paint the vertical grid lines if necessary.
388         if (table.getShowVerticalLines()) {
389             g.setColor(table.getGridColor());
390             int x1 = vacatedColumnRect.x;
391             int y1 = vacatedColumnRect.y;
392             int x2 = x1 + vacatedColumnRect.width - 1;
393             int y2 = y1 + vacatedColumnRect.height - 1;
394             // Left
395             g.drawLine(x1 - 1, y1, x1 - 1, y2);
396             // Right
397             g.drawLine(x2, y1, x2, y2);
398         }
399 
400         boolean isFocused = isFocused();
401 
402         for (int row = rMin; row <= rMax; row++) {
403             // Render the cell value
404             Rectangle r = table.getCellRect(row, draggedColumnIndex, false);
405             r.x += distance;
406             paintCell(g, r, row, draggedColumnIndex, isFocused);
407 
408             // Paint the (lower) horizontal grid line if necessary.
409             if (table.getShowHorizontalLines()) {
410                 g.setColor(table.getGridColor());
411                 Rectangle rcr = table.getCellRect(row, draggedColumnIndex, true);
412                 rcr.x += distance;
413                 int x1 = rcr.x;
414                 int y1 = rcr.y;
415                 int x2 = x1 + rcr.width - 1;
416                 int y2 = y1 + rcr.height - 1;
417                 g.drawLine(x1, y2, x2, y2);
418             }
419         }
420     }
421 
422     private int viewIndexForColumn(TableColumn aColumn) {
423         TableColumnModel cm = table.getColumnModel();
424         for (int column = 0; column < cm.getColumnCount(); column++) {
425             if (cm.getColumn(column) == aColumn) {
426                 return column;
427             }
428         }
429         return -1;
430     }
431 
432     private boolean isFocused() {
433         return table.isEditing() || QuaquaUtilities.isFocused(table);
434     }
435 
436     private void paintCells(Graphics g, int rMin, int rMax, int cMin, int cMax) {
437         boolean isFocused = isFocused();
438 
439         JTableHeader header = table.getTableHeader();
440         TableColumn draggedColumn = (header == null) ? null : header.getDraggedColumn();
441 
442         TableColumnModel cm = table.getColumnModel();
443         int columnMargin = cm.getColumnMargin();
444 
445         Rectangle cellRect;
446         TableColumn aColumn;
447         int columnWidth;
448         if (table.getComponentOrientation().isLeftToRight()) {
449             for (int row = rMin; row <= rMax; row++) {
450                 cellRect = table.getCellRect(row, cMin, false);
451                 for (int column = cMin; column <= cMax; column++) {
452                     aColumn = cm.getColumn(column);
453                     columnWidth = aColumn.getWidth();
454                     cellRect.width = columnWidth - columnMargin;
455                     if (aColumn != draggedColumn) {
456                         paintCell(g, cellRect, row, column, isFocused);
457                     }
458                     cellRect.x += columnWidth;
459                 }
460             }
461         } else {
462             for (int row = rMin; row <= rMax; row++) {
463                 cellRect = table.getCellRect(row, cMin, false);
464                 aColumn = cm.getColumn(cMin);
465                 if (aColumn != draggedColumn) {
466                     columnWidth = aColumn.getWidth();
467                     cellRect.width = columnWidth - columnMargin;
468                     paintCell(g, cellRect, row, cMin, isFocused);
469                 }
470                 for (int column = cMin + 1; column <= cMax; column++) {
471                     aColumn = cm.getColumn(column);
472                     columnWidth = aColumn.getWidth();
473                     cellRect.width = columnWidth - columnMargin;
474                     cellRect.x -= columnWidth;
475                     if (aColumn != draggedColumn) {
476                         paintCell(g, cellRect, row, column, isFocused);
477                     }
478                 }
479             }
480         }
481 
482         // Paint the dragged column if we are dragging.
483         if (draggedColumn != null) {
484             paintDraggedArea(g, rMin, rMax, draggedColumn, header.getDraggedDistance());
485         }
486 
487         // Remove any renderers that may be left in the rendererPane.
488         rendererPane.removeAll();
489 
490     }
491 
492     private void paintCell(Graphics g, Rectangle cellRect, int row, int column, boolean isFocused) {
493         // Ugly dirty hack to get correct painting of inactive tables
494         Color background = UIManager.getColor("Table.selectionBackground");
495         Color foreground = UIManager.getColor("Table.selectionForeground");
496         if (background instanceof InactivatableColorUIResource) {
497             ((InactivatableColorUIResource) background).setActive(isFocused &&
498                     (table.getRowSelectionAllowed() || table.getColumnSelectionAllowed()));
499         }
500         if (foreground instanceof InactivatableColorUIResource) {
501             // Note: We must draw with inactive color, if the current cell is not selected
502             //       Otherwise, we get white text on white background.
503             ((InactivatableColorUIResource) foreground).setActive(isFocused &&
504                     (table.getRowSelectionAllowed() || table.getColumnSelectionAllowed()) &&
505                     table.isCellSelected(row, column));
506         }
507 
508         Dimension spacing = table.getIntercellSpacing();
509         if (table.getShowHorizontalLines()) {
510             spacing.height -= 1;
511         }
512         if (table.getShowVerticalLines()) {
513             spacing.width -= 1;
514         }
515 
516         if (table.isEditing() && table.getEditingRow() == row &&
517                 table.getEditingColumn() == column) {
518             Component component = table.getEditorComponent();
519             //  component.setBackground((isStriped) ? getAlternateColor(row2 % 2) : table.getBackground());
520             // We only need to paint the alternate background color for even
521             // rows, because the background for uneven rows is painted by
522             // method paintViewport().
523 /*
524             if (isStriped && row2 % 2 == 0) {
525             g.setColor(getAlternateColor(0));
526             g.fillRect(cellRect.x - spacing.width, cellRect.y, cellRect.width + spacing.width * 2, cellRect.height + spacing.height);
527             }*/
528             component.setFont(table.getFont());
529             component.setBounds(cellRect);
530             component.validate();
531         } else {
532             TableCellRenderer renderer = table.getCellRenderer(row, column);
533             Component component = table.prepareRenderer(renderer, row, column);
534 
535             if (isStriped) {
536                 g.setColor(getAlternateColor(row % 2));
537                 g.fillRect(cellRect.x - spacing.width, cellRect.y, cellRect.width + spacing.width * 2, cellRect.height + spacing.height);
538             }
539             if (/*!table.isEditing() &&*/table.isCellSelected(row, column)) {
540                 g.setColor(background);
541                 g.fillRect(cellRect.x - spacing.width, cellRect.y, cellRect.width + spacing.width * 2, cellRect.height);
542             }
543 
544             if ((component instanceof UIResource) && (component instanceof JComponent)) {
545                 ((JComponent) component).setOpaque(false);
546             }
547 
548             //component.setBackground(background);
549             rendererPane.paintComponent(g, component, table, cellRect.x, cellRect.y,
550                     cellRect.width, cellRect.height, true);
551 
552         }
553         // Ugly dirty hack to get proper rendering of inactive tables
554         // Here we clean up the values of the "active" property of the selection
555         // colors.
556         if (!isFocused) {
557             if (background instanceof InactivatableColorUIResource) {
558                 ((InactivatableColorUIResource) background).setActive(true);
559             }
560             if (foreground instanceof InactivatableColorUIResource) {
561                 ((InactivatableColorUIResource) foreground).setActive(true);
562             }
563         }
564     }
565 
566     /**
567      * Creates the mouse listener for the JTable.
568      */
569     protected MouseInputListener createMouseInputListener() {
570         // FIXME - We haven't yet implemented a mouse handler for J2SE6.
571         // Only use our own mouse listener on Java 1.4 and 1.5,
572         // it does not work with J2SE6.
573         if (QuaquaManager.getProperty("Quaqua.Table.useJ2SE5MouseHandler","false").equals("true") ||
574                 QuaquaManager.getProperty("java.version").startsWith("1.4") ||
575                 QuaquaManager.getProperty("java.version").startsWith("1.5")) {
576             return new MouseHandler();
577         } else {
578             return super.createMouseInputListener();
579         }
580     }
581 
582     /**
583      * Creates the property change listener for the JTable.
584      */
585     private PropertyChangeListener createPropertyChangeListener() {
586         return getHandler();
587     }
588 
589     /**
590      * Creates the list selection listener for the JTable.
591      */
592     private ListSelectionListener createListSelectionListener() {
593         return getHandler();
594     }
595 
596     /**
597      * Creates the list selection listener for the JTable.
598      */
599     private TableColumnModelListener createTableColumnModelListener() {
600         return getHandler();
601     }
602 
603     /**
604      * Lazily creates the handler.
605      */
606     private Handler getHandler() {
607         if (handler == null) {
608             handler = new Handler();
609         }
610         return handler;
611     }
612 
613     /**
614      * Creates the focus listener for handling keyboard navigation in the JTable.
615      */
616     protected FocusListener createFocusListener() {
617         return new FocusHandler();
618     }
619     //
620     //  The Table's focus listener
621     //
622     /**
623      * This inner class is marked &quot;public&quot; due to a compiler bug.
624      * This class should be treated as a &quot;protected&quot; inner class.
625      * Instantiate it only within subclasses of BasicTableUI.
626      */
627     /**
628      * PropertyChangeListener for the table. Updates the appropriate
629      * varaible, or TreeState, based on what changes.
630      */
631     private class Handler implements
632             PropertyChangeListener, ListSelectionListener, TableColumnModelListener {
633 
634         private boolean rowSelectionAdjusting;
635 
636         public void propertyChange(PropertyChangeEvent event) {
637             String name = event.getPropertyName();
638 
639             if (name.equals("Quaqua.Table.style")) {
640                 Object value = event.getNewValue();
641                 isStriped = value != null && value.equals("striped");
642                 updateStriped();
643             } else if (name.equals("showVerticalLines") ||
644                     name.equals("showHorizontalLines")) {
645                 if (table.getParent() instanceof JViewport) {
646                     table.getParent().repaint();
647                 }
648             } else if (name.equals("selectionModel")) {
649                 if (event.getOldValue() != null) {
650                     ((ListSelectionModel) event.getOldValue()).removeListSelectionListener(listSelectionListener);
651                 }
652                 if (event.getNewValue() != null) {
653                     ((ListSelectionModel) event.getNewValue()).addListSelectionListener(listSelectionListener);
654                 }
655             } else if (name.equals("columnModel")) {
656                 if (event.getOldValue() != null) {
657                     ((TableColumnModel) event.getOldValue()).removeColumnModelListener(columnModelListener);
658                 }
659                 if (event.getNewValue() != null) {
660                     ((TableColumnModel) event.getNewValue()).addColumnModelListener(columnModelListener);
661                 }
662             } else if (name.equals("tableCellEditor")) {
663                 table.repaint();
664             } else if (name.equals("JComponent.sizeVariant")) {
665                 QuaquaUtilities.applySizeVariant(table);
666             }
667         }
668 
669         public void columnAdded(TableColumnModelEvent e) {
670         }
671 
672         public void columnRemoved(TableColumnModelEvent e) {
673         }
674 
675         public void columnMoved(TableColumnModelEvent e) {
676         }
677 
678         public void columnMarginChanged(ChangeEvent e) {
679         }
680 
681         private int getAdjustedIndex(int index, boolean row) {
682             int compare = row ? table.getRowCount() : table.getColumnCount();
683             return index < compare ? index : -1;
684         }
685 
686         public void columnSelectionChanged(ListSelectionEvent e) {
687             ListSelectionModel selectionModel = table.getSelectionModel();
688             int firstIndex = limit(e.getFirstIndex(), 0, table.getColumnCount() - 1);
689             int lastIndex = limit(e.getLastIndex(), 0, table.getColumnCount() - 1);
690             int minRow = 0;
691             int maxRow = table.getRowCount() - 1;
692             if (table.getRowSelectionAllowed()) {
693                 minRow = selectionModel.getMinSelectionIndex();
694                 maxRow = selectionModel.getMaxSelectionIndex();
695                 int leadRow = getAdjustedIndex(selectionModel.getLeadSelectionIndex(), true);
696 
697                 if (minRow == -1 || maxRow == -1) {
698                     if (leadRow == -1) {
699                         // nothing to repaint, return
700                         return;
701                     }
702 
703                     // only thing to repaint is the lead
704                     minRow = maxRow = leadRow;
705                 } else {
706                     // We need to consider more than just the range between
707                     // the min and max selected index. The lead row, which could
708                     // be outside this range, should be considered also.
709                     if (leadRow != -1) {
710                         minRow = Math.min(minRow, leadRow);
711                         maxRow = Math.max(maxRow, leadRow);
712                     }
713                 }
714             }
715             Rectangle firstColumnRect = table.getCellRect(minRow, firstIndex, false);
716             Rectangle lastColumnRect = table.getCellRect(maxRow, lastIndex, false);
717             Rectangle dirtyRegion = firstColumnRect.union(lastColumnRect);
718             Dimension intercellSpacing = table.getIntercellSpacing();
719             if (intercellSpacing != null) {
720                 dirtyRegion.width += table.getIntercellSpacing().width;
721             }
722             table.repaint(dirtyRegion);
723         }
724 
725         /**
726          * This is a reimplementation of the JTable.valueChanged method,
727          * with the only difference, that we repaint the cells _including_ the
728          * intercell spacing.
729          *
730          * @param e
731          */
732         public void valueChanged(ListSelectionEvent e) {
733             boolean isAdjusting = e.getValueIsAdjusting();
734             if (rowSelectionAdjusting && !isAdjusting) {
735                 // The assumption is that when the model is no longer adjusting
736                 // we will have already gotten all the changes, and therefore
737                 // don't need to do an additional paint.
738                 rowSelectionAdjusting = false;
739                 return;
740             }
741             rowSelectionAdjusting = isAdjusting;
742             // The getCellRect() calls will fail unless there is at least one column.
743             if (table.getRowCount() <= 0 || table.getColumnCount() <= 0) {
744                 return;
745             }
746             int firstIndex = limit(e.getFirstIndex(), 0, table.getRowCount() - 1);
747             int lastIndex = limit(e.getLastIndex(), 0, table.getRowCount() - 1);
748             Rectangle firstRowRect = table.getCellRect(firstIndex, 0, true);
749             Rectangle lastRowRect = table.getCellRect(lastIndex, table.getColumnCount() - 1, true);
750             Rectangle dirtyRegion = firstRowRect.union(lastRowRect);
751             dirtyRegion.width += table.getIntercellSpacing().width;
752             table.repaint(dirtyRegion);
753         }
754 
755         private int limit(int i, int a, int b) {
756             return Math.min(b, Math.max(i, a));
757         }
758     } // End of BasicTableUI.Handler
759 
760     public class MouseHandler implements MouseInputListener {
761 
762         // Component receiving mouse events during editing.
763         // May not be editorComponent.
764         private Component dispatchComponent;
765         private boolean selectedOnPress;
766         private boolean mouseReleaseDeselects;
767         private final static int MOUSE_DRAG_DOES_NOTHING = 0;
768         private final static int MOUSE_DRAG_SELECTS = 1;
769         private final static int MOUSE_DRAG_TOGGLES_SELECTION = 2;
770         private int mouseDragAction;
771         /** index of previously toggled row. */
772         private int toggledRow = -1;
773         /** index of previously toggled column. */
774         private int toggledColumn = -1;
775 
776         //  The Table's mouse listener methods.
777         public void mouseClicked(MouseEvent e) {
778         }
779 
780         private void setDispatchComponent(MouseEvent e) {
781             Component editorComponent = table.getEditorComponent();
782             Point p = e.getPoint();
783             Point p2 = SwingUtilities.convertPoint(table, p, editorComponent);
784             dispatchComponent = SwingUtilities.getDeepestComponentAt(editorComponent,
785                     p2.x, p2.y);
786         }
787 
788         private boolean repostEvent(MouseEvent e) {
789             // Check for isEditing() in case another event has
790             // caused the editor to be removed. See bug #4306499.
791             if (dispatchComponent == null || !table.isEditing()) {
792                 return false;
793             }
794             MouseEvent e2 = SwingUtilities.convertMouseEvent(table, e, dispatchComponent);
795             dispatchComponent.dispatchEvent(e2);
796             return true;
797         }
798 
799         private void setValueIsAdjusting(boolean flag) {
800             table.getSelectionModel().setValueIsAdjusting(flag);
801             table.getColumnModel().getSelectionModel().setValueIsAdjusting(flag);
802         }
803 
804         private boolean shouldIgnore(MouseEvent e) {
805             return e.isConsumed() || (!(SwingUtilities.isLeftMouseButton(e) && table.isEnabled())) || e.isPopupTrigger() &&
806                     (table.rowAtPoint(e.getPoint()) == -1 ||
807                     table.isRowSelected(table.rowAtPoint(e.getPoint())));
808         }
809 
810         public void mousePressed(MouseEvent e) {
811             mouseDragAction = MOUSE_DRAG_DOES_NOTHING;
812             mouseReleaseDeselects = false;
813             toggledRow = toggledColumn = -1;
814 
815             Point p = e.getPoint();
816             int row = table.rowAtPoint(p);
817             int column = table.columnAtPoint(p);
818 
819             // Note: We must check for table.editCellAt, regardless whether
820             // the table is currently editing or not.
821             //---if (! table.isEditing()) {
822 //            table.requestFocus();
823             if (table.editCellAt(row, column, e)) {
824                 setDispatchComponent(e);
825                 repostEvent(e);
826             }
827             //---}
828 
829             // Note: Some applications depend on selection changes only occuring
830             // on focused components. Maybe we must not do any changes to the
831             // selection changes at all, when the compnent is not focused?
832             table.requestFocusInWindow();
833 
834             if (row != -1 && column != -1) {
835                 if (table.isRowSelected(row) && e.isPopupTrigger()) {
836                     // Do not change the selection, if the item is already
837                     // selected, and the user triggers the popup menu.
838                 } else {
839                     int anchorIndex = table.getSelectionModel().getAnchorSelectionIndex();
840                     // Workaround: Java 1.4 incorrectly reports mouse button 3 down when the Meta-Key is pressed
841                     String javaVersion = System.getProperty("java.version");
842                     if (javaVersion.startsWith("1.4") && (e.getModifiersEx() & (MouseEvent.META_DOWN_MASK | MouseEvent.BUTTON2_DOWN_MASK)) == MouseEvent.META_DOWN_MASK
843                     || ! javaVersion.startsWith("1.4") && (e.getModifiersEx() & (MouseEvent.META_DOWN_MASK | MouseEvent.BUTTON2_DOWN_MASK | MouseEvent.BUTTON3_DOWN_MASK)) == MouseEvent.META_DOWN_MASK) {
844                         // toggle the selection
845                         table.changeSelection(row, column, true, false);
846                         toggledRow = row;
847                         toggledColumn = column;
848                         mouseDragAction = MOUSE_DRAG_TOGGLES_SELECTION;
849                     } else if ((e.getModifiersEx() & (MouseEvent.SHIFT_DOWN_MASK | MouseEvent.BUTTON2_DOWN_MASK | MouseEvent.BUTTON3_DOWN_MASK)) == MouseEvent.SHIFT_DOWN_MASK &&
850                             anchorIndex != -1) {
851                         // add all rows to the selection from the anchor to the row
852                         table.changeSelection(row, column, false, true);
853                         //table.setRowSelectionInterval(anchorIndex, row);
854                         mouseDragAction = MOUSE_DRAG_SELECTS;
855                     } else if ((e.getModifiersEx() & (MouseEvent.SHIFT_DOWN_MASK | MouseEvent.META_DOWN_MASK)) == 0) {
856                         if (table.isCellSelected(row, column)) {
857                             mouseReleaseDeselects = table.isFocusOwner();
858                         } else {
859                             // Only select the cell
860                             table.changeSelection(row, column, false, false);
861                             //table.setRowSelectionInterval(row, row);
862                             mouseDragAction = MOUSE_DRAG_SELECTS;
863                         }
864                     //table.getSelectionModel().setAnchorSelectionIndex(row);
865                     }
866                 }
867             }
868 
869             table.getSelectionModel().setValueIsAdjusting(mouseDragAction != MOUSE_DRAG_DOES_NOTHING);
870         /*
871         if (e.isConsumed()) {
872         selectedOnPress = false;
873         return;
874         }
875         selectedOnPress = true;
876         mouseReleaseDeselects = true;
877         adjustFocusAndSelection(e);
878          */
879         }
880 
881         public void mouseReleased(MouseEvent e) {
882             repostEvent(e);
883             mouseDragAction = MOUSE_DRAG_DOES_NOTHING;
884             if (mouseReleaseDeselects) {
885                 int row = table.rowAtPoint(e.getPoint());
886                 int column = table.columnAtPoint(e.getPoint());
887                 table.changeSelection(row, column, false, false);
888             }
889             table.getSelectionModel().setValueIsAdjusting(false);
890 
891             if (table.isRequestFocusEnabled() && !table.isEditing()) {
892                 table.requestFocus();
893             }
894         }
895 
896         public void mouseEntered(MouseEvent e) {
897         }
898 
899         public void mouseExited(MouseEvent e) {
900         }
901 
902         //  The Table's mouse motion listener methods.
903         public void mouseMoved(MouseEvent e) {
904         }
905 
906         public void mouseDragged(MouseEvent e) {
907             if (shouldIgnore(e)) {
908                 return;
909             }
910             /*
911             mouseReleaseDeselects = false;
912             repostEvent(e);
913             CellEditor editor = table.getCellEditor();
914             if (editor == null || editor.shouldSelectCell(e)) {
915             Point p = e.getPoint();
916             int row2 = table.rowAtPoint(p);
917             int column = table.columnAtPoint(p);
918             // The autoscroller can generate drag events outside the Table's range.
919             if ((column == -1) || (row2 == -1)) {
920             return;
921             }
922             // Fix for 4835633
923             // Until we support drag-selection, dragging should not change
924             // the selection (act like single-select).
925             Object bySize = table.getClientProperty("Table.isFileList");
926             if (bySize instanceof Boolean &&
927             ((Boolean) bySize).booleanValue()) {
928             return;
929             }
930             table.changeSelection(row2, column, false, true);
931             }*/
932             CellEditor editor = table.getCellEditor();
933             if (editor == null || editor.shouldSelectCell(e)) {
934                 mouseReleaseDeselects = false;
935                 if (mouseDragAction == MOUSE_DRAG_SELECTS) {
936                     int row = table.rowAtPoint(e.getPoint());
937                     int column = table.columnAtPoint(e.getPoint());
938                     if (row != -1 && column != -1) {
939                         Rectangle cellBounds = table.getCellRect(row, column, true);
940                         table.scrollRectToVisible(cellBounds);
941                         table.changeSelection(row, column, false, true);
942                     }
943                 } else if (mouseDragAction == MOUSE_DRAG_TOGGLES_SELECTION) {
944                     int row = table.rowAtPoint(e.getPoint());
945                     int column = table.columnAtPoint(e.getPoint());
946                     boolean isCellSelection = table.getCellSelectionEnabled();
947                     if (row != -1 && column != -1 &&
948                             ((!isCellSelection && row != toggledRow) ||
949                             (isCellSelection && (row != toggledRow || column != toggledColumn)))) {
950                         Rectangle cellBounds = table.getCellRect(row, column, true);
951                         table.scrollRectToVisible(cellBounds);
952                         table.changeSelection(row, column, true, false);
953                         toggledRow = row;
954                         toggledColumn = column;
955                     }
956                 }
957             }
958         }
959     }
960 
961     private class FocusHandler implements FocusListener {
962         // FocusListener
963         private void repaintSelection() {
964             int[] rows = table.getSelectedRows();
965             Rectangle dirtyRect = null;
966             for (int r = 0; r < rows.length; r++) {
967                 for (int c = 0,  n = table.getColumnCount(); c < n; c++) {
968                     table.repaint(table.getCellRect(rows[r], c, false));
969                 }
970             }
971         }
972 
973         public void focusGained(FocusEvent e) {
974             repaintSelection();
975         }
976 
977         public void focusLost(FocusEvent e) {
978             repaintSelection();
979         }
980     }
981 }
982