1 package com.jbidwatcher.ui.table;
2 /*
3  * @(#)TableSorter.java 1.5 97/12/17
4  *
5  * Copyright (c) 1997 Sun Microsystems, Inc. All Rights Reserved.
6  *
7  * This software is the confidential and proprietary information of Sun
8  * Microsystems, Inc. ("Confidential Information").  You shall not
9  * disclose such Confidential Information and shall use it only in
10  * accordance with the terms of the license agreement you entered into
11  * with Sun.
12  *
13  * SUN MAKES NO REPRESENTATIONS OR WARRANTIES ABOUT THE SUITABILITY OF THE
14  * SOFTWARE, EITHER EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE
15  * IMPLIED WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR
16  * PURPOSE, OR NON-INFRINGEMENT. SUN SHALL NOT BE LIABLE FOR ANY DAMAGES
17  * SUFFERED BY LICENSEE AS A RESULT OF USING, MODIFYING OR DISTRIBUTING
18  * THIS SOFTWARE OR ITS DERIVATIVES.
19  *
20  */
21 
22 /**
23  * A sorter for AbstractSortableTableModels. The sorter has a model (conforming to AbstractSortableTableModel)
24  * and itself implements AbstractSortableTableModel. TableSorter does not store or copy
25  * the data in the AbstractSortableTableModel, instead it maintains an array of
26  * integers which it keeps the same size as the number of rows in its
27  * model. When the model changes it notifies the sorter that something
28  * has changed eg. "rowsAdded" so that its internal array of integers
29  * can be reallocated. As requests are made of the sorter (like
30  * getSortByValueAt(row, col) it redirects them to its model via the mapping
31  * array. That way the TableSorter appears to hold another copy of the table
32  * with the rows in a different order. The sorting algorthm used is stable
33  * which means that it does not move around rows when its comparison
34  * function returns 0 to denote that they are equivalent.
35  *
36  * @version 1.5 12/17/97
37  * @author Philip Milne
38  */
39 
40 import com.jbidwatcher.util.config.JConfig;
41 
42 import java.util.*;
43 import javax.swing.event.TableModelEvent;
44 import javax.swing.event.TableModelListener;
45 
46 // Imports for picking up mouse events from the JTable.
47 
48 import java.awt.event.MouseAdapter;
49 import java.awt.event.MouseEvent;
50 import java.awt.event.InputEvent;
51 import java.awt.*;
52 import javax.swing.table.*;
53 import javax.swing.*;
54 
55 public class TableSorter extends Transformation implements TableModelListener {
56   private JTable _table = null;
57   private ColumnStateList columnStateList;
58   private BaseTransformation _model = null;
59   private SortTransformation _sorted = null;
60 
61   private static final Icon ascend = new ImageIcon(JConfig.getResource("/icons/ascend_10x5.gif"));
62   private static final Icon descend = new ImageIcon(JConfig.getResource("/icons/descend_10x5.gif"));
63 
TableSorter(String name, String defaultColumn, BaseTransformation tm)64   public TableSorter(String name, String defaultColumn, BaseTransformation tm) {
65     _model = tm;
66     columnStateList = new ColumnStateList();
67     _sorted = new SortTransformation(_model);
68     m_tm = _sorted;
69     setDefaults(name, defaultColumn);
70   }
71 
tableChanged(TableModelEvent e)72   public void tableChanged(TableModelEvent e) {
73     fireTableChanged(e);
74   }
75 
sort()76   public void sort() {
77     final TableSorter sorter = this;
78 
79     SwingUtilities.invokeLater(new Runnable() {
80       public void run() {
81         Selection save = new Selection(_table, _sorted);
82         _sorted.sort();
83         _table.tableChanged(new TableModelEvent(sorter));
84         restoreSelection(save);
85       }
86     });
87   }
88 
sortByList()89   private void sortByList() {
90     _sorted.setSortList(columnStateList);
91 
92     if(_table != null) sort();
93   }
94 
setDefaults(String inName, String defaultColumn)95   private void setDefaults(String inName, String defaultColumn) {
96     columnStateList.clear();
97 
98 	  for(int i=0; ; i++) {
99       String sortByColumn;
100       String sortDirection;
101       if(i==0) {
102 			  // Initially sort by ending time, ascending.
103 			  sortByColumn = JConfig.queryDisplayProperty(inName + ".sort_by", defaultColumn);
104 			  sortDirection = JConfig.queryDisplayProperty(inName + ".sort_direction", "ascending");
105 		  } else {
106 			  sortByColumn = JConfig.queryDisplayProperty(inName + ".sort_by" + "_" + i);
107 			  sortDirection = JConfig.queryDisplayProperty(inName + ".sort_direction" + "_" + i);
108 		  }
109 
110 		  if(sortByColumn == null || sortDirection == null) {
111 			  break;
112 		  }
113 
114       ColumnState cs = new ColumnState(getColumnNumber(sortByColumn), sortDirection.equals("ascending") ? 1 : -1);
115 
116       if(columnStateList.indexOf(cs) == -1) columnStateList.add(cs);
117 	  }
118 
119 	  sortByList();
120   }
121 
getRowCount()122   public int getRowCount() {  return m_tm.getRowCount(); }
getColumnCount()123   public int getColumnCount() { return m_tm.getColumnCount(); }
getValueAt(int row, int col)124   public synchronized Object getValueAt(int row, int col) { return m_tm.getValueAt(row, col); }
125 
delete(Object o)126   public boolean delete(Object o) {
127     final int myRow;
128     synchronized(this) {
129       myRow = m_tm.findRow(o);
130     }
131 
132     final TableSorter sorter = this;
133     if(myRow == -1) return false;
134     final Selection save = new Selection(_table, _sorted);
135     save.delete(myRow);
136     synchronized (this) {
137       m_tm.delete(myRow);
138     }
139     try {
140       SwingUtilities.invokeAndWait(new Runnable() {
141         public void run() {
142           _table.tableChanged(new TableModelEvent(sorter, myRow, myRow, TableModelEvent.ALL_COLUMNS, TableModelEvent.DELETE));
143           restoreSelection(save);
144         }
145       });
146     } catch (Exception e) {
147       e.printStackTrace();  //To change body of catch statement use File | Settings | File Templates.
148     }
149     return true;
150   }
151 
152   private static class Selection {
153     protected   final int[] selected;   // used ONLY within save/restoreSelection();
154     protected   int     lead = -1;
155     protected SortTransformation sorter;
156 
delete(int viewRow)157     protected void delete(int viewRow) {
158       int modelRow = sorter.convertRowIndexToModel(viewRow);
159       for(int i=0; i<selected.length; i++) {
160         if(selected[i] > modelRow) {
161           selected[i]--;
162         } else if(selected[i] == modelRow) {
163           selected[i] = -1;
164         }
165       }
166       if(lead > modelRow) lead--; else if(lead == modelRow) lead = -1;
167     }
168 
Selection(JTable table, SortTransformation sortBy)169     protected Selection(JTable table, SortTransformation sortBy) {
170       sorter = sortBy;
171       selected = table.getSelectedRows(); // in view coordinates
172       for (int i = 0; i < selected.length; i++) {
173         int view = selected[i];
174         selected[i] = sorter.convertRowIndexToModel(view);    // model coordinates
175       }
176 
177       if (selected.length > 0) {
178         // convert lead selection index to model coordinates
179         lead = sorter.convertRowIndexToModel(table.getSelectionModel().getLeadSelectionIndex());
180       }
181     }
182   }
183 
restoreSelection(Selection selection)184   private void restoreSelection(Selection selection) {
185     _table.clearSelection();   // call overridden version
186 
187     boolean lead_selected = false;
188     for (int i = 0; i < selection.selected.length; i++) {
189       int selected = selection.selected[i];
190       if(selected != selection.lead && selected != -1) {
191           int index = _sorted.convertRowIndexToView(selected);
192           if(index != -1) _table.getSelectionModel().addSelectionInterval(index, index);
193       }
194       if(selected == selection.lead) lead_selected = true;
195     }
196 
197     if(selection.lead >= 0) {
198       int new_lead = _sorted.convertRowIndexToView(selection.lead);
199       if(new_lead != -1) {
200         if(lead_selected) {
201           _table.getSelectionModel().addSelectionInterval(new_lead, new_lead);
202         } else {
203           _table.getSelectionModel().removeSelectionInterval(new_lead, new_lead);
204         }
205       }
206     }
207   }
208 
insert(Object o)209   public int insert(Object o) {
210     final Selection save = getSelectionSafely();
211 
212     int myRow;
213     synchronized(this) {
214       myRow = m_tm.insert(o);
215     }
216     if(myRow == -1) return -1;
217 
218     final TableSorter sorter = this;
219     final int count = m_tm.getRowCount();
220 
221     SwingUtilities.invokeLater(new Runnable() {
222       public void run() {
223         _table.tableChanged(new TableModelEvent(sorter, 0, count, TableModelEvent.ALL_COLUMNS, TableModelEvent.INSERT));
224         if(save != null) restoreSelection(save);
225       }
226     });
227     return myRow;
228   }
229 
getSelectionSafely()230   private Selection getSelectionSafely() {
231     Selection save;
232     try {
233       save = new Selection(_table, _sorted);
234     } catch(Exception e) {
235       save = null;
236     }
237     return save;
238   }
239 
update(final Object updated)240   public boolean update(final Object updated) {
241     final int myRow;
242     synchronized(this) {
243       myRow = m_tm.findRow(updated);
244     }
245     final TableSorter sorter = this;
246     if (myRow != -1) {
247       SwingUtilities.invokeLater(new Runnable() {
248         public void run() {
249           Selection save = new Selection(_table, _sorted);
250           _table.tableChanged(new TableModelEvent(sorter, myRow));
251           restoreSelection(save);
252         }
253       });
254     }
255     return myRow != -1;
256   }
257 
updateTime()258   public void updateTime() {
259     final TableSorter sorter = this;
260     SwingUtilities.invokeLater(new Runnable() {
261       public void run() {
262         _table.tableChanged(new TableModelEvent(sorter, 0, _sorted.getRowCount(), getColumnNumber("Time left")));
263       }
264     });
265   }
266 
267   private static class SortHeaderRenderer extends JLabel implements TableCellRenderer {
SortHeaderRenderer()268     public SortHeaderRenderer() {
269       setHorizontalTextPosition(LEFT);
270       setHorizontalAlignment(CENTER);
271     }
272 
getTableCellRendererComponent(JTable table, Object value, boolean isSelected, boolean hasFocus, int row, int col)273     public Component getTableCellRendererComponent(JTable table, Object value,
274                                                    boolean isSelected, boolean hasFocus,
275                                                    int row, int col) {
276       if (table != null) {
277         JTableHeader header = table.getTableHeader();
278         if (header != null) {
279           setForeground(header.getForeground());
280           setBackground(header.getBackground());
281           setFont(header.getFont());
282         }
283       }
284 
285       setText((value == null) ? "" : value.toString());
286       setBorder(UIManager.getBorder("TableHeader.cellBorder"));
287 
288       return this;
289     }
290   }
291 
getSortProperties(String prefix, Properties outProps)292   public Properties getSortProperties(String prefix, Properties outProps) {
293     TableColumnModel tableColumnModel = _table.getColumnModel();
294     for(int i=0; i < columnStateList.size(); i++) {
295       ColumnState columnState = columnStateList.get(i);
296 
297       // Restore original header
298       int viewCol = _table.convertColumnIndexToView(columnState.getColumn());
299       if(viewCol != -1) {
300         TableColumn tableColumn = tableColumnModel.getColumn(viewCol);
301         tableColumn.setHeaderValue(columnState.getHeaderValue());
302 
303         outProps.setProperty(prefix + ".sort_by" + (i > 0 ? "_" + i : ""), _model.getColumnName(columnState.getColumn()));
304         outProps.setProperty(prefix + ".sort_direction" + (i > 0 ? "_" + i : ""), columnState.getSort() == 1 ? "ascending" : "descending");
305       }
306     }
307 
308     return outProps;
309   }
310 
setArrow(TableColumnModel tcm, int col, int direction)311   private void setArrow(TableColumnModel tcm, int col, int direction) {
312     if(col == -1) return;
313     //col = _table.convertColumnIndexToModel(col);
314     TableColumn tc = tcm.getColumn(col);
315     TableCellRenderer tcr = tc.getHeaderRenderer();
316 
317     if (tcr == null || !(tcr instanceof SortHeaderRenderer)) {
318       tcr = new SortHeaderRenderer();
319       tc.setHeaderRenderer(tcr);
320     }
321 
322     SortHeaderRenderer shr = (SortHeaderRenderer) tcr;
323 
324     switch (direction) {
325       case -1:
326         shr.setIcon(ascend);
327         break;
328       case 0:
329         shr.setIcon(null);
330         break;
331       case 1:
332         shr.setIcon(descend);
333         break;
334         // Can't happen, because the result set is only -1,
335         //  0, 1, but static analysis can't determine that.
336       default:
337         break;
338     }
339   }
340 
341   // There is no-where else to put this.
342   // Add a mouse listener to the Table to trigger a table sort
343   // when a column heading is clicked in the JTable.
addMouseListenerToHeaderInTable(JTable table)344   public void addMouseListenerToHeaderInTable(JTable table) {
345     TableColumnModel tableColumnModel = table.getColumnModel();
346 
347     _table = table;
348 
349     // Restore the header as it was saved
350     for(int i=0; i < columnStateList.size(); i++) {
351       ColumnState columnState = columnStateList.get(i);
352       int viewCol = table.convertColumnIndexToView(columnState.getColumn());
353       if(viewCol != -1) {
354         TableColumn tableColumn = tableColumnModel.getColumn(viewCol);
355 
356         // Save original header
357         String headerValue = (String) tableColumn.getHeaderValue();
358         columnState.setHeaderValue(headerValue);
359         // Set new header
360         tableColumn.setHeaderValue(headerValue + (i > 0 ? " (" + (i + 1) + ")" : ""));
361         tableColumn.setIdentifier(headerValue);
362         // Set arrow
363         setArrow(tableColumnModel, table.convertColumnIndexToView(columnState.getColumn()), columnState.getSort());
364       }
365     }
366 
367     table.setColumnSelectionAllowed(false);
368     MouseAdapter listMouseListener = new SortMouseAdapter(table, this);
369     table.getTableHeader().addMouseListener(listMouseListener);
370   }
371 
removeColumn(String colId, JTable table)372   public void removeColumn(String colId, JTable table) {
373     for(int i=0; i<columnStateList.size(); i++) {
374       ColumnState cs = columnStateList.get(i);
375       if(cs.getHeaderValue().equals(colId)) {
376         columnStateList.remove(cs);
377         i--;
378       }
379       refreshColumns(table, false);
380     }
381   }
382 
refreshColumns(JTable table, boolean resetHeaders)383   private void refreshColumns(JTable table, boolean resetHeaders) {
384     int skipped = 0;
385     TableColumnModel columnModel = table.getColumnModel();
386     for(int i=0; i<columnStateList.size(); i++) {
387       ColumnState cs = columnStateList.get(i);
388       int view = table.convertColumnIndexToView(cs.getColumn());
389       if(view != -1) {
390         TableColumn tc = columnModel.getColumn(view);
391 
392         if(resetHeaders) {
393           setArrow(columnModel, view, 0);
394           tc.setHeaderValue(cs.getHeaderValue());
395         } else {
396           tc.setHeaderValue(cs.getHeaderValue() + (i > 0 ? " (" + (i + 1 - skipped) + ")" : ""));
397         }
398       } else {
399         skipped++;
400       }
401     }
402   }
403 
getObjectAt(int x, int y)404   public Object getObjectAt(int x, int y) {
405     if (_table != null) {
406       int rowPoint = _table.rowAtPoint(new Point(x, y));
407 
408       //  A menu item has been selected, instead of a context menu.
409       //  This is NOT a valid test, because the popup locations aren't
410       //  reset!
411       if (x == 0 && y == 0) {
412         rowPoint = _table.getSelectedRow();
413       }
414 
415       if (rowPoint != -1) {
416         return getValueAt(rowPoint, -1);
417       }
418     }
419     return null;
420   }
421 
select(Selector s)422   public boolean select(Selector s) {
423     return s.select(_table);
424   }
425 
getSelectedRows()426   public int[] getSelectedRows() {
427     return _table.getSelectedRows();
428   }
429 
getTable()430   public JTable getTable() { return _table; }
431 
432   private class SortMouseAdapter extends MouseAdapter
433   {
434     private final JTable mTable;
435     private final TableSorter mSorter;
436 
SortMouseAdapter(JTable table, TableSorter sorter)437     public SortMouseAdapter(JTable table, TableSorter sorter) {
438       mTable = table;
439       mSorter = sorter;
440     }
441 
mouseClicked(MouseEvent e)442     public void mouseClicked(MouseEvent e) {
443       TableColumnModel columnModel = mTable.getColumnModel();
444       int viewColumn = columnModel.getColumnIndexAtX(e.getX());
445       TableColumn tc = columnModel.getColumn(viewColumn);
446       int modelColumn = mTable.convertColumnIndexToModel(viewColumn);
447 
448       if(e.getClickCount() == 1) {
449         ColumnState columnState = new ColumnState(modelColumn);
450         int csidx = columnStateList.indexOf(columnState);
451         if ((e.getModifiers() & InputEvent.CTRL_MASK) != InputEvent.CTRL_MASK) {
452           if(columnStateList.size() > 1 || csidx == -1) {
453             refreshColumns(mTable, true);
454             columnStateList.clear();
455             columnState.setSortState(1);
456           } else {
457             if(columnStateList.size() == 1) {
458               columnState = columnStateList.get(0);
459               columnState.setSortState(columnState.getSort()==1?-1:1);
460             }
461           }
462           columnState.setHeaderValue((String) tc.getHeaderValue());
463           setArrow(columnModel, viewColumn, columnState.getSort());
464           columnStateList.add(columnState);
465         } else {
466           if (csidx == -1) {
467             // Not yet sorted by this column, add to list
468             columnState.setHeaderValue((String) tc.getHeaderValue());
469             columnStateList.add(columnState);
470           } else {
471             columnState = columnStateList.get(csidx);
472           }
473 
474           // Transition to next sort (undefined -> asc, asc -> desc, desc -> undefined
475           int state = columnState.setNextSortState();
476 
477           setArrow(columnModel, viewColumn, state);
478 
479           if (state == 0) {
480             // Restore original header
481             tc.setHeaderValue(columnState.getHeaderValue());
482             // Reached undef state again so remove from sort list
483             columnStateList.remove(columnState);
484           }
485 
486           // Renumber new / rest of headers accordingly
487           refreshColumns(mTable, false);
488         }
489 
490         mSorter.sortByList();
491         mTable.getTableHeader().repaint();
492       }
493     }
494   }
495 
enableInsertionSorting()496   public void enableInsertionSorting() {
497     _sorted.sortOnInsert();
498   }
499 }
500