View Javadoc

1   /*
2    *  XNap Commons
3    *
4    *  Copyright (C) 2005  Steffen Pingel
5    *
6    *  This library is free software; you can redistribute it and/or
7    *  modify it under the terms of the GNU Lesser General Public
8    *  License as published by the Free Software Foundation; either
9    *  version 2.1 of the License, or (at your option) any later version.
10   *
11   *  This library is distributed in the hope that it will be useful,
12   *  but WITHOUT ANY WARRANTY; without even the implied warranty of
13   *  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
14   *  Lesser General Public License for more details.
15   *
16   *  You should have received a copy of the GNU Lesser General Public
17   *  License along with this library; if not, write to the Free Software
18   *  Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
19   */
20  package org.xnap.commons.gui.table;
21  
22  import java.awt.Color;
23  import java.awt.Component;
24  import java.awt.Graphics;
25  import java.awt.event.InputEvent;
26  import java.awt.event.MouseEvent;
27  import java.awt.event.MouseListener;
28  import java.awt.event.MouseMotionListener;
29  import java.beans.PropertyChangeEvent;
30  import java.beans.PropertyChangeListener;
31  import java.util.ArrayList;
32  import java.util.Enumeration;
33  import java.util.Hashtable;
34  import java.util.Iterator;
35  import java.util.List;
36  import javax.swing.Icon;
37  import javax.swing.JLabel;
38  import javax.swing.JPopupMenu;
39  import javax.swing.JTable;
40  import javax.swing.JTree;
41  import javax.swing.SwingUtilities;
42  import javax.swing.UIManager;
43  import javax.swing.event.ChangeEvent;
44  import javax.swing.event.ListSelectionEvent;
45  import javax.swing.event.TableColumnModelEvent;
46  import javax.swing.event.TableColumnModelListener;
47  import javax.swing.table.JTableHeader;
48  import javax.swing.table.TableCellRenderer;
49  import javax.swing.table.TableColumn;
50  import javax.swing.table.TableColumnModel;
51  import javax.swing.tree.TreePath;
52  import org.xnap.commons.gui.table.SortableModel.Order;
53  import org.xnap.commons.gui.util.GUIHelper;
54  
55  /***
56   * TODO update documentation
57   * TODO squig don't make TableSorter a requirement, maybe have a general
58   * table layout and a more specific SortableTableLayout subclass?
59   * TableHeaderHandler class handles the mouse events invoked by clicking on the
60   * header portion of the JTable.
61   * TODO setter for maintain sort order
62   * TODO call stopCellEditing() whenever neccessary
63   */
64  public class TableLayout {
65  
66  	private List<TableColumn> columns;
67  	private EventHandler eventHandler;
68  	private Enumeration expandedLeafs;
69  	private JTableHeader header;
70  	private JPopupMenu headerPopupMenu;
71  	private List<TableLayoutListener> listeners = new ArrayList<TableLayoutListener>();
72  	private SortButtonRenderer renderer;
73  	private TreePath[] selectedPaths;
74  	private TableSorter sorter;
75  	/***
76  	 * Set to true, when the user has pressed a button but not yet dragged it.
77  	 */
78  	private boolean sorting = false;
79  	private JTable table;
80  	private JTree tree;
81  	
82  	public TableLayout(JTable table) {
83  		this(table, (TableSorter)table.getModel());
84  	}
85  
86  	/***
87  	 * 
88  	 */
89  	public TableLayout(JTable table, TableSorter sorter)
90  	{
91  		this.table = table;
92  		this.header = table.getTableHeader();
93  		this.sorter = sorter;
94  		this.eventHandler = new EventHandler();
95  		
96  		TableColumnModel columnModel = table.getColumnModel();
97  		columns = new ArrayList<TableColumn>(columnModel.getColumnCount());
98  		for (int i = 0; i < columnModel.getColumnCount(); i++) {
99  			addColumn(columnModel.getColumn(i));
100 		}
101 		columnModel.addColumnModelListener(eventHandler);
102 		headerPopupMenu = new TableHeaderMenu(null, this).getPopupMenu();
103 		renderer = new SortButtonRenderer(header.getDefaultRenderer());
104 		header.setDefaultRenderer(renderer);
105 		header.setReorderingAllowed(true);
106 		header.setResizingAllowed(true);
107 		header.addMouseListener(eventHandler);
108 		header.addMouseMotionListener(eventHandler);
109 	}
110 
111 	private void addColumn(TableColumn column) {
112 		column.addPropertyChangeListener(eventHandler);
113 		columns.add(column);
114 	}
115 	
116 	public void addTableLayoutListener(TableLayoutListener l) {
117 		listeners.add(l);
118 	}
119 
120 	// TODO raise visiblity to protected
121 	private void fireColumnNameChanged(int index, String newName)
122 	{
123 		TableLayoutListener[] array = listeners.toArray(new TableLayoutListener[0]);
124 		for (TableLayoutListener listener : array) {
125 			listener.columnNameChanged(index, newName);
126 		}
127 	}
128 
129 	private void fireColumnOrderChanged()
130 	{
131 		TableLayoutListener[] array = listeners.toArray(new TableLayoutListener[0]);
132 		for (TableLayoutListener listener : array) {
133 			listener.columnOrderChanged();
134 		}
135 	}
136 
137 	private void fireColumnVisibilityChanged(int index, boolean visible)
138 	{
139 		TableLayoutListener[] array = listeners.toArray(new TableLayoutListener[0]);
140 		for (TableLayoutListener listener : array) {
141 			listener.columnVisibilityChanged(index, visible);
142 		}
143 	}
144 
145 	private void fireColumnWidthsChanged()
146 	{
147 		TableLayoutListener[] array = listeners.toArray(new TableLayoutListener[0]);
148 		for (TableLayoutListener listener : array) {
149 			listener.columnLayoutChanged();
150 		}
151 	}
152 
153 	private void fireSortedColumnChanged(int col)
154 	{
155 		TableLayoutListener[] array = listeners.toArray(new TableLayoutListener[0]);
156 		for (TableLayoutListener listener : array) {
157 			listener.sortedColumnChanged();
158 		}
159 	}
160 
161 	public TableColumn getColumnAt(int index)
162 	{
163 		return columns.get(index);
164 	}
165 
166 	public int getColumnCount()
167 	{
168 		return columns.size();
169 	}
170 	
171 	public int getColumnIndex(String identifier)
172 	{
173 		for (int i = 0; i < columns.size(); i++) {
174 			if (getColumnAt(i).getIdentifier().equals(identifier)) {
175 				return i;
176 			}
177 		}
178 		return -1;
179 	}
180 
181 	public Iterator<TableColumn> getColumns()
182 	{
183 		return columns.iterator();
184 	}
185 
186 	public boolean getMaintainSortOrder()
187 	{
188 		return sorter.getMaintainSortOrder();
189 	}
190 
191 	public int getSortedColumn()
192 	{
193 		return sorter.getSortedColumn();
194 	}
195 	
196 	public JTable getTable()
197 	{
198 		return table;
199 	}
200 
201 	public JPopupMenu getHeaderPopupMenu()
202 	{
203 		return headerPopupMenu;
204 	}
205 	
206 	public int getVisibleColumnsCount()
207 	{
208 		return table.getColumnModel().getColumnCount();
209 	}
210 
211 	public boolean isColumnVisible(int index)
212 	{
213 		TableColumn column = getColumnAt(index);
214 		TableColumnModel columnModel = table.getColumnModel();
215 		for (int i = 0; i < columnModel.getColumnCount(); i++) {
216 			if (columnModel.getColumn(i) == column) {
217 				return true;
218 			}
219 		}		
220 		return false;
221 	}
222 
223 	public Order getSortOrder()
224 	{
225 		return sorter.getSortOrder();
226 	}
227 
228 	public void removeTableLayoutListener(TableLayoutListener l) {
229 		listeners.remove(l);
230 	}
231 
232 	public void restoreSelections()
233 	{
234 		if (tree != null) {
235 			if (expandedLeafs != null) {
236 				while (expandedLeafs.hasMoreElements()) {
237 					TreePath path = (TreePath)expandedLeafs.nextElement();
238 					tree.expandPath(path);
239 				}
240 			}
241 			if (selectedPaths != null) {
242 				tree.addSelectionPaths(selectedPaths);
243 			}
244 		}
245 	}
246 	
247 	public void setAllColumnsVisible(boolean visible)
248 	{
249 		for (int i = 0; i < getColumnCount(); i++) {
250 			setColumnVisible(i, visible);
251 		}
252 	}
253 
254 	/***
255 	 * Sets the auto resize mode of the table depending on a modifier key.
256 	 */
257 	private void setAutoResizeMode(MouseEvent e)
258 	{
259 		if ((e.getModifiers() & InputEvent.SHIFT_MASK) != 0) {
260 			table.setAutoResizeMode(JTable.AUTO_RESIZE_NEXT_COLUMN);
261 		}
262 		else if ((e.getModifiers() & InputEvent.CTRL_MASK) != 0) {
263 			table.setAutoResizeMode(JTable.AUTO_RESIZE_ALL_COLUMNS);
264 		}
265 		else {
266 			table.setAutoResizeMode(JTable.AUTO_RESIZE_SUBSEQUENT_COLUMNS);
267 		}
268 	}
269 	
270 	public void setColumnNames(String[] columnNames)
271 	{
272 		if (columns.size() != columnNames.length) {
273 			throw new IllegalArgumentException("Number of columns and length of columnNames must match");
274 		}
275 		
276 		for (int i = 0; i < columnNames.length; i++) {
277 			columns.get(i).setHeaderValue(columnNames[i]);
278 		}
279 	}
280 
281 	public void setColumnName(int index, String name)
282 	{
283 		TableColumn column = columns.get(index);
284 		column.setHeaderValue(name);
285 	}
286 
287 	public void setColumnProperties(int index, String key)
288 	{
289 		TableColumn column = columns.get(index);
290 		column.setIdentifier(key);
291 	}
292 
293 	public void setColumnProperties(int index, String key, int width)
294 	{
295 		TableColumn column = columns.get(index);
296 		column.setIdentifier(key);
297 		column.setPreferredWidth(width);
298 		column.setWidth(width);
299 	}
300 	
301 	public void setColumnVisible(int index, boolean visible)
302 	{
303 		boolean currentlyVisible = isColumnVisible(index);
304 		if (currentlyVisible == visible) {
305 			return;
306 		}
307 		
308 		if (visible) {
309 			// TODO add the column at a sensible position
310 			table.getColumnModel().addColumn(getColumnAt(index));
311 		} else {
312 			table.getColumnModel().removeColumn(getColumnAt(index));
313 		}
314 		fireColumnVisibilityChanged(index, visible);
315 	}
316 	
317 	public void setColumnsVisible(String[] columns)
318 	{
319 		setAllColumnsVisible(false);
320 		for (int i = 0; i < columns.length; i++) {
321 			int index = getColumnIndex(columns[i]);
322 			if (index != -1) {
323 				setColumnVisible(index, true);
324 			}
325 		}
326 	}
327 	
328 	public void setMaintainSortOrder(boolean maintainSortOrder)
329 	{
330 		sorter.setMaintainSortOrder(maintainSortOrder);
331 	}
332 	
333 	public void setTree(JTree jt)
334 	{
335 		this.tree = jt;
336 	}
337 	
338 	/***
339 	 * Displays the popup menu.
340 	 */
341 	public void showPopupMenu(MouseEvent e)
342 	{
343 		if (headerPopupMenu != null) {
344 			header.setDraggedColumn(null);
345 			header.setResizingColumn(null);
346 			GUIHelper.showPopupMenu(headerPopupMenu, e.getComponent(),
347 					e.getX(), e.getY());
348 		}
349 	}
350 
351 	public void sortByColumn(int modelIndex, SortableModel.Order sortOrder, boolean revert) {
352 		sortByColumn(table.convertColumnIndexToView(modelIndex), modelIndex, sortOrder, revert);
353 	}
354 
355 	private void sortByColumn(int column, int modelIndex, SortableModel.Order sortOrder, boolean revert) {
356 		stopCellEditing();
357 		storeSelections();
358 		sortOrder = sorter.sortByColumn(modelIndex, sortOrder, revert);
359 		renderer.setSortedColumn(column, sortOrder);
360 		restoreSelections();
361 	}
362 
363 	/***
364 	 * 
365 	 */
366 	private void sortByColumnInternal(int column)
367 	{
368 		int modelIndex = table.convertColumnIndexToModel(column);
369 		SortableModel.Order sortOrder;
370 		if (sorter.getSortedColumn() == modelIndex) {
371 			// keep old sort order
372 			sortOrder = sorter.getSortOrder();
373 		}
374 		else if (sorter.getColumnClass(modelIndex) == String.class) {
375 			// sort strings ascending
376 			sortOrder = SortableModel.Order.ASCENDING;
377 		}
378 		else {
379 			// sort descending by default
380 			sortOrder = SortableModel.Order.DESCENDING;
381 		}
382 		// revert if the same column was resorted
383 		boolean revert = (sorter.getSortedColumn() == modelIndex);
384 		sortByColumn(column, modelIndex, sortOrder, revert);
385 	}
386 
387 	public void storeSelections()
388 	{
389 		if (tree != null) {
390 			selectedPaths = tree.getSelectionPaths();
391 			expandedLeafs = tree.getExpandedDescendants(new TreePath(tree
392 					.getModel().getRoot()));
393 		}
394 	}
395 
396 	public void stopCellEditing()
397 	{
398 		if (table.getCellEditor() != null) {
399 			table.getCellEditor().stopCellEditing();
400 		}
401 	}
402 
403 	/***
404 	 * Provides a small arrow shaped icon.
405 	 */
406 	private static class BevelArrowIcon implements Icon {
407 		
408 		public static final int DOWN  = 1;
409 		public static final int UP    = 0;
410 		
411 		private int direction;
412 		private Color edge1;
413 		private Color edge2;
414 		
415 		public BevelArrowIcon(int direction, boolean isPressedView) 
416 		{
417 			this.direction = direction;
418 			
419 			if (isPressedView) {
420 				edge1 = UIManager.getColor("controlDkShadow");
421 				edge2 = UIManager.getColor("controlLtHighlight");
422 			} 
423 			else {
424 				edge1 = UIManager.getColor("controlShadow");
425 				edge2 = UIManager.getColor("controlHighlight");
426 			}
427 		}
428 		
429 		public int getIconHeight() 
430 		{
431 			return 7;
432 		}
433 		
434 		public int getIconWidth() 
435 		{
436 			return 8;
437 		}
438 		
439 		public void paintIcon(Component c, Graphics g, int x, int y) 
440 		{
441 			switch (direction) {
442 			case DOWN:
443 				g.setColor(edge2);
444 				g.drawLine(x + 5, y + 7, x + 8, y);
445 				g.setColor(edge1);
446 				g.drawLine(x, y, x + 8, y);
447 				g.drawLine(x, y, x + 4, y + 7);
448 				break;
449 			case UP:
450 				g.setColor(edge1);
451 				g.drawLine(x, y + 6, x + 4, y);
452 				g.setColor(edge2);
453 				g.drawLine(x, y + 7, x + 8, y + 7);
454 				g.drawLine(x + 5, y, x + 8, y + 7);
455 				break;
456 			}
457 		}	
458 		
459 	}
460 
461 	protected class EventHandler implements
462 		MouseListener, MouseMotionListener, TableColumnModelListener, 
463 		PropertyChangeListener {
464 
465 
466 		public void columnAdded(TableColumnModelEvent event)
467 		{
468 		}
469 
470 		public void columnMarginChanged(ChangeEvent event)
471 		{
472 		}
473 
474 		public void columnMoved(TableColumnModelEvent event)
475 		{
476 			int i = table.convertColumnIndexToView(sorter.getSortedColumn());
477 			if (i != -1) {
478 				renderer.setSortedColumn(i, sorter.getSortOrder());
479 			}
480 			fireColumnOrderChanged();
481 		}
482 
483 		public void columnRemoved(TableColumnModelEvent event)
484 		{
485 		}
486 
487 		public void columnSelectionChanged(ListSelectionEvent event)
488 		{
489 		}
490 
491 		public void mouseClicked(MouseEvent e)
492 		{
493 		}
494 
495 		public void mouseDragged(MouseEvent e)
496 		{
497 			if (sorting) {
498 				// abort sorting
499 				sorting = false;
500 				renderer.selectColumn(-1);
501 				header.repaint();
502 			}
503 			setAutoResizeMode(e);
504 		}
505 
506 		public void mouseEntered(MouseEvent e)
507 		{
508 		}
509 
510 		public void mouseExited(MouseEvent e)
511 		{
512 		}
513 
514 		public void mouseMoved(MouseEvent e)
515 		{
516 		}
517 
518 		public void mousePressed(MouseEvent e)
519 		{
520 			if (e.isPopupTrigger()) {
521 				showPopupMenu(e);
522 			}
523 			else {
524 				int col = header.columnAtPoint(e.getPoint());
525 				renderer.selectColumn(col);
526 				header.repaint();
527 				sorting = true;
528 			}
529 			setAutoResizeMode(e);
530 		}
531 
532 		public void mouseReleased(MouseEvent e)
533 		{
534 			if (e.isPopupTrigger()) {
535 				showPopupMenu(e);
536 			}
537 			else if (sorting) {
538 				int col = header.columnAtPoint(e.getPoint());
539 				if (table.isEditing()) {
540 					table.getCellEditor().stopCellEditing();
541 				}
542 				sortByColumnInternal(col);
543 				fireSortedColumnChanged(col);
544 				sorting = false;
545 				renderer.selectColumn(-1);
546 				header.repaint();
547 			}
548 			else {
549 				fireColumnWidthsChanged();
550 			}
551 		}
552 
553 		public void propertyChange(PropertyChangeEvent evt)
554 		{
555 			if ("headerValue".equals(evt.getPropertyName())) {
556 				TableColumn column = (TableColumn)evt.getSource();
557 				fireColumnNameChanged(getColumnIndex(column.getIdentifier().toString()), evt.getNewValue().toString());
558 			}
559 		}
560 
561 	}
562 
563 	/***
564 	 * Don't use this class directly. Use 
565 	 * <code>TableHeaderListener.install()</code> instead.
566 	 *
567 	 * <p>If an instance of this class is set as the header renderer for a table, 
568 	 *
569 	 * @see xnap.gui.table.TableHeaderListener
570 	 */
571 	private static class SortButtonRenderer implements TableCellRenderer
572 	{
573 		
574 		public static final Icon downIcon = new BevelArrowIcon(BevelArrowIcon.DOWN, false);
575 		public static final Icon upIcon = new BevelArrowIcon(BevelArrowIcon.UP, false);
576 		
577 		private Hashtable<Integer, Icon> icons = new Hashtable<Integer, Icon>();
578 		private JLabel label;
579 		private TableCellRenderer renderer;
580 		private int selectedColumn = -1;
581 		
582 		/***
583 		 * Constructs a SortButtonRenderer. The <code>renderer</code> is used
584 		 * to renderer the header. This needs to be an instance of 
585 		 * {@link javax.swing.JLabel JLabel} for the sort arrow to be displayed.
586 		 *
587 		 * <p>This implementation uses the setIcon() method of JLabel to display 
588 		 * the arrow icon. Make sure you do not use the icon of the renderer.
589 		 *
590 		 * @param renderer the default renderer
591 		 */
592 		public SortButtonRenderer(TableCellRenderer renderer)
593 		{
594 			this.renderer = renderer;
595 			if (renderer instanceof JLabel) {
596 				label = (JLabel)renderer;
597 				label.setHorizontalTextPosition(SwingUtilities.LEFT);
598 			}
599 			else {
600 				// avoid null pointer exception if cr is not instanceof JLabel
601 				label = new JLabel();
602 			}
603 		}
604 		
605 		/***
606 		 * Returns the icon of col.  
607 		 */
608 		public Icon getIcon(int col) 
609 		{
610 			return (Icon)icons.get(new Integer(col));
611 		}
612 		
613 		public Component getTableCellRendererComponent(JTable table, Object value,
614 				boolean isSelected, boolean hasFocus,
615 				int row, int column) 
616 		{
617 			// set the 
618 			label.setIcon(getIcon(column));
619 			
620 			Component c = renderer.getTableCellRendererComponent
621 			(table, value, isSelected, hasFocus, row, column);
622 			
623 			if (column == selectedColumn) {
624 				label.setBackground(UIManager.getColor("controlShadow"));
625 			}
626 			
627 			return c;
628 		}
629 		
630 		public void selectColumn(int col)
631 		{
632 			selectedColumn = col;
633 		}
634 		
635 		/***
636 		 * Prints the arrow icon to the column at index <code>col</code>.
637 		 */
638 		public void setSortedColumn(int col, Order sortOrder) 
639 		{
640 			icons.clear();
641 			if (sortOrder == SortableModel.Order.ASCENDING) {
642 				icons.put(new Integer(col), downIcon);
643 			}
644 			else if (sortOrder == SortableModel.Order.DESCENDING) {
645 				icons.put(new Integer(col), upIcon);
646 			}
647 		}
648 	}
649 
650 }