View Javadoc

1   package ch.elca.el4j.services.gui.model.tablemodel;
2   import java.awt.*;
3   import java.awt.event.*;
4   import java.util.*;
5   import java.util.List;
6   
7   import javax.swing.*;
8   import javax.swing.event.TableModelEvent;
9   import javax.swing.event.TableModelListener;
10  import javax.swing.table.*;
11  
12  import org.jdesktop.swingbinding.validation.ValidatedProperty;
13  
14  import ch.elca.el4j.util.codingsupport.annotations.FindBugsSuppressWarnings;
15  
16  /**
17   * TableSorter is a decorator for TableModels; adding sorting
18   * functionality to a supplied TableModel. TableSorter does
19   * not store or copy the data in its TableModel; instead it maintains
20   * a map from the row indexes of the view to the row indexes of the
21   * model. As requests are made of the sorter (like getValueAt(row, col))
22   * they are passed to the underlying model after the row numbers
23   * have been translated via the internal mapping array. This way,
24   * the TableSorter appears to hold another copy of the table
25   * with the rows in a different order.
26   * <p/>
27   * TableSorter registers itself as a listener to the underlying model,
28   * just as the JTable itself would. Events received from the model
29   * are examined, sometimes manipulated (typically widened), and then
30   * passed on to the TableSorter's listeners (typically the JTable).
31   * If a change to the model has invalidated the order of TableSorter's
32   * rows, a note of this is made and the sorter will resort the
33   * rows the next time a value is requested.
34   * <p/>
35   * When the tableHeader property is set, either by using the
36   * setTableHeader() method or the two argument constructor, the
37   * table header may be used as a complete UI for TableSorter.
38   * The default renderer of the tableHeader is decorated with a renderer
39   * that indicates the sorting status of each column. In addition,
40   * a mouse listener is installed with the following behavior:
41   * <ul>
42   * <li>
43   * Mouse-click: Clears the sorting status of all other columns
44   * and advances the sorting status of that column through three
45   * values: {NOT_SORTED, ASCENDING, DESCENDING} (then back to
46   * NOT_SORTED again).
47   * <li>
48   * SHIFT-mouse-click: Clears the sorting status of all other columns
49   * and cycles the sorting status of the column through the same
50   * three values, in the opposite order: {NOT_SORTED, DESCENDING, ASCENDING}.
51   * <li>
52   * CONTROL-mouse-click and CONTROL-SHIFT-mouse-click: as above except
53   * that the changes to the column do not cancel the statuses of columns
54   * that are already sorting - giving a way to initiate a compound
55   * sort.
56   * </ul>
57   * <p/>
58   * This is a long overdue rewrite of a class of the same name that
59   * first appeared in the swing table demos in 1997.
60   *
61   * @svnLink $Revision: 4068 $;$Date: 2010-01-05 09:38:21 +0100 (Di, 05. Jan 2010) $;$Author: jonasha $;$URL: https://el4j.svn.sourceforge.net/svnroot/el4j/branches/el4j_3_1/el4j/framework/modules/swing/src/main/java/ch/elca/el4j/services/gui/model/tablemodel/TableSorter.java $
62   *
63   * @author Philip Milne
64   * @author Brendon McLean
65   * @author Dan van Enckevort
66   * @author Parwinder Sekhon
67   * @version 2.0 02/27/04
68   * 
69   * @see ch.elca.el4j.demos.gui.MasterDetailDemoForm for a demonstration of its use.
70   * 
71   */
72  
73  // SWI: suppressChangeEvents added
74  
75  public class TableSorter extends AbstractTableModel {
76  	/**
77  	 *
78  	 */
79  	private static final long serialVersionUID = 1L;
80  
81  	protected TableModel tableModel;
82  
83  	public static final int DESCENDING = -1;
84  	public static final int NOT_SORTED = 0;
85  	public static final int ASCENDING = 1;
86  
87  	private static Directive EMPTY_DIRECTIVE = new Directive(-1, NOT_SORTED);
88  
89  	public static final Comparator COMPARABLE_COMAPRATOR = new Comparator() {
90  		public int compare(Object o1, Object o2) {
91  			return ((Comparable) o1).compareTo(o2);
92  		}
93  	};
94  	public static final Comparator LEXICAL_COMPARATOR = new Comparator() {
95  		public int compare(Object o1, Object o2) {
96  			return o1.toString().compareTo(o2.toString());
97  		}
98  	};
99  
100 	private Row[] viewToModel;
101 	private int[] modelToView;
102 
103 	private JTableHeader tableHeader;
104 	private MouseListener mouseListener;
105 	private TableModelListener tableModelListener;
106 	private Map columnComparators = new HashMap();
107 	private List sortingColumns = new ArrayList();
108 	private boolean suppressChangeEvents = false;
109 
110 	public TableSorter() {
111 		this.mouseListener = new MouseHandler();
112 		this.tableModelListener = new TableModelHandler();
113 	}
114 
115 	public TableSorter(TableModel tableModel) {
116 		this();
117 		setTableModel(tableModel);
118 	}
119 
120 	public TableSorter(TableModel tableModel, JTableHeader tableHeader) {
121 		this();
122 		setTableHeader(tableHeader);
123 		setTableModel(tableModel);
124 	}
125 
126 	private void clearSortingState() {
127 		viewToModel = null;
128 		modelToView = null;
129 	}
130 
131 	public TableModel getTableModel() {
132 		return tableModel;
133 	}
134 
135 	public void setTableModel(TableModel tableModel) {
136 		if (this.tableModel != null) {
137 			this.tableModel.removeTableModelListener(tableModelListener);
138 		}
139 
140 		this.tableModel = tableModel;
141 		if (this.tableModel != null) {
142 			this.tableModel.addTableModelListener(tableModelListener);
143 		}
144 
145 		clearSortingState();
146 		fireTableStructureChanged();
147 	}
148 
149 	public JTableHeader getTableHeader() {
150 		return tableHeader;
151 	}
152 
153 	public void setTableHeader(JTableHeader tableHeader) {
154 		if (this.tableHeader != null) {
155 			this.tableHeader.removeMouseListener(mouseListener);
156 			TableCellRenderer defaultRenderer = this.tableHeader.getDefaultRenderer();
157 			if (defaultRenderer instanceof SortableHeaderRenderer) {
158 				this.tableHeader.setDefaultRenderer(((SortableHeaderRenderer) defaultRenderer).tableCellRenderer);
159 			}
160 		}
161 		this.tableHeader = tableHeader;
162 		if (this.tableHeader != null) {
163 			this.tableHeader.addMouseListener(mouseListener);
164 			this.tableHeader.setDefaultRenderer(
165 					new SortableHeaderRenderer(this.tableHeader.getDefaultRenderer()));
166 		}
167 	}
168 
169 	public boolean isSorting() {
170 		return sortingColumns.size() != 0;
171 	}
172 
173 	private Directive getDirective(int column) {
174 		for (int i = 0; i < sortingColumns.size(); i++) {
175 			Directive directive = (Directive)sortingColumns.get(i);
176 			if (directive.column == column) {
177 				return directive;
178 			}
179 		}
180 		return EMPTY_DIRECTIVE;
181 	}
182 
183 	public int getSortingStatus(int column) {
184 		return getDirective(column).direction;
185 	}
186 
187 	private void sortingStatusChanged() {
188 		clearSortingState();
189 		fireTableDataChanged();
190 		if (tableHeader != null) {
191 			tableHeader.repaint();
192 		}
193 	}
194 
195 	public void setSortingStatus(int column, int status) {
196 		Directive directive = getDirective(column);
197 		if (directive != EMPTY_DIRECTIVE) {
198 			sortingColumns.remove(directive);
199 		}
200 		if (status != NOT_SORTED) {
201 			sortingColumns.add(new Directive(column, status));
202 		}
203 		sortingStatusChanged();
204 	}
205 
206 	protected Icon getHeaderRendererIcon(int column, int size) {
207 		Directive directive = getDirective(column);
208 		if (directive == EMPTY_DIRECTIVE) {
209 			return null;
210 		}
211 		return new Arrow(directive.direction == DESCENDING, size, sortingColumns.indexOf(directive));
212 	}
213 
214 	private void cancelSorting() {
215 		sortingColumns.clear();
216 		sortingStatusChanged();
217 	}
218 
219 	public void setColumnComparator(Class type, Comparator comparator) {
220 		if (comparator == null) {
221 			columnComparators.remove(type);
222 		} else {
223 			columnComparators.put(type, comparator);
224 		}
225 	}
226 
227 	protected Comparator getComparator(int column) {
228 		Class columnType = tableModel.getColumnClass(column);
229 		Comparator comparator = (Comparator) columnComparators.get(columnType);
230 		if (comparator != null) {
231 			return comparator;
232 		}
233 		if (Comparable.class.isAssignableFrom(columnType)) {
234 			return COMPARABLE_COMAPRATOR;
235 		}
236 		return LEXICAL_COMPARATOR;
237 	}
238 
239 	private Row[] getViewToModel() {
240 		if (viewToModel == null) {
241 			int tableModelRowCount = tableModel.getRowCount();
242 			viewToModel = new Row[tableModelRowCount];
243 			for (int row = 0; row < tableModelRowCount; row++) {
244 				viewToModel[row] = new Row(row);
245 			}
246 
247 			if (isSorting()) {
248 				Arrays.sort(viewToModel);
249 			}
250 		}
251 		return viewToModel;
252 	}
253 
254 	public int modelIndex(int viewIndex) {
255 		return getViewToModel()[viewIndex].modelIndex;
256 	}
257 
258 	private int[] getModelToView() {
259 		if (modelToView == null) {
260 			int n = getViewToModel().length;
261 			modelToView = new int[n];
262 			for (int i = 0; i < n; i++) {
263 				modelToView[modelIndex(i)] = i;
264 			}
265 		}
266 		return modelToView;
267 	}
268 
269 	// TableModel interface methods
270 
271 	public int getRowCount() {
272 		return (tableModel == null) ? 0 : tableModel.getRowCount();
273 	}
274 
275 	public int getColumnCount() {
276 		return (tableModel == null) ? 0 : tableModel.getColumnCount();
277 	}
278 
279 	public String getColumnName(int column) {
280 		return tableModel.getColumnName(column);
281 	}
282 
283 	public Class getColumnClass(int column) {
284 		return tableModel.getColumnClass(column);
285 	}
286 
287 	public boolean isCellEditable(int row, int column) {
288 		return tableModel.isCellEditable(modelIndex(row), column);
289 	}
290 
291 	public Object getValueAt(int row, int column) {
292 		return tableModel.getValueAt(modelIndex(row), column);
293 	}
294 
295 	public void setValueAt(Object aValue, int row, int column) {
296 		tableModel.setValueAt(aValue, modelIndex(row), column);
297 	}
298 
299 	// Helper classes
300 	
301 	private class Row implements Comparable {
302 		private int modelIndex;
303 
304 		public Row(int index) {
305 			this.modelIndex = index;
306 		}
307 
308 		public int compareTo(Object o) {
309 			int row1 = modelIndex;
310 			int row2 = ((Row) o).modelIndex;
311 
312 			for (Iterator it = sortingColumns.iterator(); it.hasNext();) {
313 				Directive directive = (Directive) it.next();
314 				int column = directive.column;
315 				Object o1 = tableModel.getValueAt(row1, column);
316 				Object o2 = tableModel.getValueAt(row2, column);
317 				
318 				// SWI: make compare aware of validation
319 				if (o1 instanceof ValidatedProperty) {
320 					ValidatedProperty vp = (ValidatedProperty) o1;
321 					o1 = vp.getValue();
322 				}
323 				if (o2 instanceof ValidatedProperty) {
324 					ValidatedProperty vp = (ValidatedProperty) o2;
325 					o2 = vp.getValue();
326 				}
327 
328 				int comparison = 0;
329 				// Define null less than everything, except null.
330 				if (o1 == null && o2 == null) {
331 					comparison = 0;
332 				} else if (o1 == null) {
333 					comparison = -1;
334 				} else if (o2 == null) {
335 					comparison = 1;
336 				} else if (o1 instanceof String && o2 instanceof String) {
337 					comparison = ((String) o1).compareToIgnoreCase((String) o2);
338 				} else {
339 					comparison = getComparator(column).compare(o1, o2);
340 				}
341 				if (comparison != 0) {
342 					return directive.direction == DESCENDING ? -comparison : comparison;
343 				}
344 			}
345 			return 0;
346 		}
347 	}
348 
349 	private class TableModelHandler implements TableModelListener {
350 		public void tableChanged(TableModelEvent e) {
351 			if (suppressChangeEvents) {
352 				// a table cell gets edited -> don't forward tableChange.
353 				return;
354 			}
355 			// If we're not sorting by anything, just pass the event along.
356 			if (!isSorting()) {
357 				clearSortingState();
358 				fireTableChanged(e);
359 				return;
360 			}
361 				
362 			// If the table structure has changed, cancel the sorting; the
363 			// sorting columns may have been either moved or deleted from
364 			// the model.
365 			if (e.getFirstRow() == TableModelEvent.HEADER_ROW) {
366 				cancelSorting();
367 				fireTableChanged(e);
368 				return;
369 			}
370 
371 			// We can map a cell event through to the view without widening
372 			// when the following conditions apply:
373 			//
374 			// a) all the changes are on one row (e.getFirstRow() == e.getLastRow()) and,
375 			// b) all the changes are in one column (column != TableModelEvent.ALL_COLUMNS) and,
376 			// c) we are not sorting on that column (getSortingStatus(column) == NOT_SORTED) and,
377 			// d) a reverse lookup will not trigger a sort (modelToView != null)
378 			//
379 			// Note: INSERT and DELETE events fail this test as they have column == ALL_COLUMNS.
380 			//
381 			// The last check, for (modelToView != null) is to see if modelToView
382 			// is already allocated. If we don't do this check; sorting can become
383 			// a performance bottleneck for applications where cells
384 			// change rapidly in different parts of the table. If cells
385 			// change alternately in the sorting column and then outside of
386 			// it this class can end up re-sorting on alternate cell updates -
387 			// which can be a performance problem for large tables. The last
388 			// clause avoids this problem.
389 			int column = e.getColumn();
390 			if (e.getFirstRow() == e.getLastRow()
391 					&& column != TableModelEvent.ALL_COLUMNS
392 					&& getSortingStatus(column) == NOT_SORTED
393 					&& modelToView != null) {
394 				int viewIndex = getModelToView()[e.getFirstRow()];
395 				fireTableChanged(new TableModelEvent(TableSorter.this,
396 					viewIndex, viewIndex,
397 					column, e.getType()));
398 				return;
399 			}
400 
401 			// Something has happened to the data that may have invalidated the row order.
402 			clearSortingState();
403 			fireTableDataChanged();
404 			return;
405 		}
406 	}
407 
408 	private class MouseHandler extends MouseAdapter {
409 		public void mouseClicked(MouseEvent e) {
410 			JTableHeader h = (JTableHeader) e.getSource();
411 			TableColumnModel columnModel = h.getColumnModel();
412 			int viewColumn = columnModel.getColumnIndexAtX(e.getX());
413 			int column = columnModel.getColumn(viewColumn).getModelIndex();
414 			if (column != -1) {
415 				int status = getSortingStatus(column);
416 				if (!e.isControlDown()) {
417 					cancelSorting();
418 				}
419 				// Cycle the sorting states through {NOT_SORTED, ASCENDING, DESCENDING} or
420 				// {NOT_SORTED, DESCENDING, ASCENDING} depending on whether shift is pressed.
421 				status = status + (e.isShiftDown() ? -1 : 1);
422 				status = (status + 4) % 3 - 1; // signed mod, returning {-1, 0, 1}
423 				setSortingStatus(column, status);
424 			}
425 		}
426 	}
427 
428 	private static class Arrow implements Icon {
429 		private boolean descending;
430 		private int size;
431 		private int priority;
432 
433 		public Arrow(boolean descending, int size, int priority) {
434 			this.descending = descending;
435 			this.size = size;
436 			this.priority = priority;
437 		}
438 
439 		@FindBugsSuppressWarnings(value = "ICAST_IDIV_CAST_TO_DOUBLE",
440 									justification = "False positive.")	
441 		public void paintIcon(Component c, Graphics g, int x, int y) {
442 			Color color = c == null ? Color.GRAY : c.getBackground();
443 			// In a compound sort, make each succesive triangle 20%
444 			// smaller than the previous one.
445 			int dx = (int)(size/2*Math.pow(0.8, priority));
446 			int dy = descending ? dx : -dx;
447 			// Align icon (roughly) with font baseline.
448 			int yaligned = y + 5*size/6 + (descending ? -dy : 0);
449 			int shift = descending ? 1 : -1;
450 			g.translate(x, yaligned);
451 
452 			// Right diagonal.
453 			g.setColor(color.darker());
454 			g.drawLine(dx / 2, dy, 0, 0);
455 			g.drawLine(dx / 2, dy + shift, 0, shift);
456 			
457 			// Left diagonal.
458 			g.setColor(color.brighter());
459 			g.drawLine(dx / 2, dy, dx, 0);
460 			g.drawLine(dx / 2, dy + shift, dx, shift);
461 			
462 			// Horizontal line.
463 			if (descending) {
464 				g.setColor(color.darker().darker());
465 			} else {
466 				g.setColor(color.brighter().brighter());
467 			}
468 			g.drawLine(dx, 0, 0, 0);
469 
470 			g.setColor(color);
471 			g.translate(-x, -yaligned);
472 		}
473 
474 		public int getIconWidth() {
475 			return size;
476 		}
477 
478 		public int getIconHeight() {
479 			return size;
480 		}
481 	}
482 
483 	private class SortableHeaderRenderer implements TableCellRenderer {
484 		private TableCellRenderer tableCellRenderer;
485 
486 		public SortableHeaderRenderer(TableCellRenderer tableCellRenderer) {
487 			this.tableCellRenderer = tableCellRenderer;
488 		}
489 
490 		public Component getTableCellRendererComponent(JTable table,
491 			Object value,
492 			boolean isSelected,
493 			boolean hasFocus,
494 			int row,
495 			int column) {
496 			
497 			Component c = tableCellRenderer.getTableCellRendererComponent(table,
498 					value, isSelected, hasFocus, row, column);
499 			if (c instanceof JLabel) {
500 				JLabel l = (JLabel) c;
501 				l.setHorizontalTextPosition(JLabel.LEFT);
502 				int modelColumn = table.convertColumnIndexToModel(column);
503 				l.setIcon(getHeaderRendererIcon(modelColumn, l.getFont().getSize()));
504 			}
505 			return c;
506 		}
507 	}
508 
509 	private static class Directive {
510 		private int column;
511 		private int direction;
512 
513 		public Directive(int column, int direction) {
514 			this.column = column;
515 			this.direction = direction;
516 		}
517 	}
518 
519 	public boolean isSuppressChangeEvents() {
520 		return suppressChangeEvents;
521 	}
522 
523 	public void setSuppressChangeEvents(boolean suppress) {
524 		suppressChangeEvents = suppress;
525 		if (!suppress) {
526 			sortingStatusChanged();
527 		}
528 	}
529 }