001    package util.swing;
002    
003    import java.util.*;
004    
005    import javax.swing.table.TableModel;
006    import javax.swing.event.TableModelEvent;
007    
008    import java.awt.event.MouseAdapter;
009    import java.awt.event.MouseEvent;
010    import java.awt.event.InputEvent;
011    import javax.swing.JTable;
012    import javax.swing.table.JTableHeader;
013    import javax.swing.table.TableColumnModel;
014    
015    /**
016     * A sorter for TableModels. The sorter has a model (conforming to TableModel)
017     * and itself implements TableModel. TableSorter does not store or copy
018     * the data in the TableModel, instead it maintains an array of
019     * integers which it keeps the same size as the number of rows in its
020     * model. When the model changes it notifies the sorter that something
021     * has changed eg. "rowsAdded" so that its internal array of integers
022     * can be reallocated. As requests are made of the sorter (like
023     * getValueAt(row, col) it redirects them to its model via the mapping
024     * array. That way the TableSorter appears to hold another copy of the table
025     * with the rows in a different order. The sorting algorthm used is stable
026     * which means that it does not move around rows when its comparison
027     * function returns 0 to denote that they are equivalent.
028     *
029     * @version 1.6 2003-03-25
030     * @author Philip Milne
031     * @author Thomas Medack
032     * @author Andreas Bartho
033     */
034    public class TableSorter extends TableMap {
035    
036        /**
037         * Field of indexes.
038         */
039        private int indexes[], reverseIndexes[];
040    
041        /**
042         * Sorting vector.
043         */
044        private Vector sortingColumns = new Vector();
045    
046        /**
047         * Sort ascending or descending.
048         */
049        private boolean ascending = true;
050    
051        /**
052         * The column to be sorted.
053         */
054        private int actualColumn = 0;
055    
056        /**
057         * Internal helper variable.
058         */
059        private int compares;
060    
061        /**
062         * Constructor.
063         */
064        public TableSorter() {
065            indexes = new int[0]; // for consistency
066        }
067    
068        /**
069         * Constructor.
070         * @param model the wrapped {@link TableModel}
071         */
072        public TableSorter(AbstractTableModel model) {
073            setModel(model);
074        }
075    
076        /**
077         * Sets the {@link TableModel}.
078         *
079         * @param model the model.
080         */
081        public void setModel(AbstractTableModel model) {
082            super.setModel(model);
083            reallocateIndexes();
084        }
085    
086        /**
087         * Compares columns of two specific rows.
088         * @param row1 first row.
089         * @param row2 second row.
090         * @param column the column index.
091         * @return a comparative value.
092         */
093        public int compareRowsByColumn(int row1, int row2, int column) {
094            Class type = model.getColumnClass(column);
095            TableModel data = model;
096            // Check for nulls.
097            Object o1 = data.getValueAt(row1, column);
098            Object o2 = data.getValueAt(row2, column);
099            // If both values are null, return 0.
100            if (o1 == null && o2 == null) {
101                return 0;
102            } else {
103                if (o1 == null) { // Define null less than everything.
104                    return -1;
105                } else {
106                    if (o2 == null) {
107                        return 1;
108                    }
109                }
110            }
111            String s1 = o1.toString();
112            String s2 = o2.toString();
113    
114            if ((o1 instanceof Comparable) && (o2 instanceof Comparable)) {
115                java.util.Comparator cmp = model.getEntryDescriptor().getColumnOrder(column);
116                return (cmp == null) ? ((Comparable)o1).compareTo(o2) : cmp.compare(model.getRecord(row1),
117                        model.getRecord(row2));
118            } else {
119                int result = s1.compareTo(s2);
120                if (result < 0) {
121                    return -1;
122                } else {
123                    if (result > 0) {
124                        return 1;
125                    } else {
126                        return 0;
127                    }
128                }
129            }
130        }
131    
132        /**
133         * Comparison of two rows.
134         *
135         * @param row1 first row.
136         * @param row2 second row.
137         * @return a comparative value.
138         */
139        public int compare(int row1, int row2) {
140            compares++;
141            for (int level = 0; level < sortingColumns.size(); level++) {
142                Integer column = (Integer)sortingColumns.elementAt(level);
143                int result = compareRowsByColumn(row1, row2, column.intValue());
144                if (result != 0) {
145                    return ascending ? result : -result;
146                }
147            }
148            return 0;
149        }
150    
151        /**
152         * Helper method.
153         */
154        private void reallocateIndexes() {
155            int rowCount = model.getRowCount();
156            // Set up a new array of indexes with the right number of elements
157            // for the new data model.
158            indexes = new int[rowCount];
159            // Initialize with the identity mapping.
160            for (int row = 0; row < rowCount; row++) {
161                indexes[row] = row;
162            }
163        }
164    
165        /**
166         * Reacts on TableChangeEvents, either converts them as needed or passes them to the model.
167         * @param e the event.
168         */
169        public void tableChanged(TableModelEvent e) {
170            if (indexes.length == getModel().getRowCount() && indexes.length != 0) {
171                createReverseIndexes();
172                super.tableChanged(new TableModelEvent(this, reverseIndexes[e.getFirstRow()]));
173            } else {
174                //if big changes happened (row add or remove) pass event to parent
175                reallocateIndexes();
176                //sortByColumn(actualColumn, ascending);
177                super.tableChanged(e);
178            }
179        }
180    
181        /**
182         * Creates a reverse index table.
183         *
184         * This method maps the line number for each table to have the view updated
185         *
186         * Example: indexes = [2 3 0 1 4]<br>
187         * reverseIndex stores in i, where i appeares in indexes:<br>
188         * 0 is on position 2, 1 is on position 3, 2 is on position 0 ... => reverseIndex = [2 3 0 1 4]<br>
189         * Mapping this array with the row number sent by a TableChangeEvent event, all tables have the
190         * correct lines updated (has only effect on CountingStocks)
191         */
192        private void createReverseIndexes() {
193            reverseIndexes = new int[indexes.length];
194            for (int i = 0; i < indexes.length; i++) {
195                for (int j = 0; j < indexes.length; j++) {
196                    if (indexes[j] == i) {
197                        reverseIndexes[i] = j;
198                    }
199                }
200            }
201        }
202    
203        /**
204         * Helper method.
205         */
206        private void checkModel() {
207            if (indexes.length != model.getRowCount()) {
208                System.err.println("Sorter not informed of a change in model.");
209            }
210        }
211    
212        /**
213         * Sorts the model. It uses {@link shuttlesort(int, int, int, int) shuttlesort}.
214         */
215        private void sort() {
216            checkModel();
217            compares = 0;
218            shuttlesort((int[])indexes.clone(), indexes, 0, indexes.length);
219        }
220    
221        // This is a home-grown implementation which we have not had time
222        // to research - it may perform poorly in some circumstances. It
223        // requires twice the space of an in-place algorithm and makes
224        // NlogN assigments shuttling the values between the two
225        // arrays. The number of compares appears to vary between N-1 and
226        // NlogN depending on the initial order but the main reason for
227        // using it here is that, unlike qsort, it is stable.
228        /**
229         * Sorting method.
230         *
231         * @param from the unsorted index array.
232         * @param to the sorted index array (i.e. the result).
233         * @param low lowest field to be taken into consideration on sorting.
234         * @param high highest field to be taken into consideration on sorting.
235         */
236        private void shuttlesort(int from[], int to[], int low, int high) {
237            if (high - low < 2) {
238                return;
239            }
240            int middle = (low + high) / 2;
241            shuttlesort(to, from, low, middle);
242            shuttlesort(to, from, middle, high);
243    
244            int p = low;
245            int q = middle;
246    
247            if (high - low >= 4 && compare(from[middle - 1], from[middle]) <= 0) {
248                for (int i = low; i < high; i++) {
249                    to[i] = from[i];
250                }
251                return;
252            }
253    
254            // A normal merge.
255    
256            for (int i = low; i < high; i++) {
257                if (q >= high || (p < middle && compare(from[p], from[q]) <= 0)) {
258                    to[i] = from[p++];
259                } else {
260                    to[i] = from[q++];
261                }
262            }
263        }
264    
265        /**
266         * Helper method. Swaps two ints.
267         *
268         * @param i first int.
269         * @param j secont int
270         */
271        private void swap(int i, int j) {
272            int tmp = indexes[i];
273            indexes[i] = indexes[j];
274            indexes[j] = tmp;
275        }
276    
277        // The mapping only affects the contents of the data rows.
278        // Pass all requests to these rows through the mapping array: "indexes".
279    
280        /**
281         * Changes the value of a table cell.
282         *
283         * @param aValue the value to set.
284         * @param aRow the row of the TableCell to be changed.
285         * @param aColumn the column of the table cell to be changed.
286         */
287        public Object getValueAt(int aRow, int aColumn) {
288            checkModel();
289            if (aRow >= 0 && model.getRowCount() > 0) {
290                return model.getValueAt(indexes[aRow], aColumn);
291            } else {
292                return null;
293            }
294        }
295    
296        /**
297         * Gets the record.
298         *
299         * @param row the affected table row.
300         * @return the appropriate record.
301         */
302        public Object getRecord(int row) {
303            checkModel();
304            if (row >= 0 && model.getRowCount() > 0 && row < model.getRowCount()) { //prevent ArrayIndexOutOfBoundException when last table entry has
305                //  disappered but is yet queried (e.g. all elements of the last
306                //  row of a TTFS table have been moved)
307                return model.getRecord(indexes[row]);
308            } else {
309                return null;
310            }
311        }
312    
313        /**
314         * Changes the value of a table cell.
315         *
316         * @param aValue the value to set.
317         * @param aRow the row of the TableCell to be changed.
318         * @param aColumn the column of the table cell to be changed.
319         */
320        public void setValueAt(Object aValue, int aRow, int aColumn) {
321            checkModel();
322            if (aRow >= 0) {
323                model.setValueAt(aValue, indexes[aRow], aColumn);
324            }
325        }
326    
327        /**
328         * Sorts the table ascending by a column.
329         *
330         * @param column the column by which the table should be sorted.
331         */
332        public void sortByColumn(int column) {
333            sortByColumn(column, true);
334        }
335    
336        /**
337         * Sorts the table by a column.
338         *
339         * @param column the column by which the table should be sorted.
340         * @param ascending if <code>true</code> sort sequence is ascending, otherwise descending.
341         */
342        public void sortByColumn(int column, boolean ascending) {
343            this.ascending = ascending;
344            sortingColumns.removeAllElements();
345            sortingColumns.addElement(new Integer(column));
346            sort();
347            super.tableChanged(new TableModelEvent(this));
348        }
349    
350        // There is no-where else to put this.
351        // Add a mouse listener to the Table to trigger a table sort
352        // when a column heading is clicked in the JTable.
353    
354        /**
355         * Adds a mouse listener to the table header.
356         *
357         * @param table the table to which header the listener is to be added
358         */
359        public void addMouseListenerToHeaderInTable(JTable table, final Object[] ao) {
360            final TableSorter sorter = this;
361            final JTable tableView = table;
362            final TableEntryDescriptor ted = getModel().getEntryDescriptor();
363            ascending = true;
364            tableView.setColumnSelectionAllowed(false);
365            MouseAdapter listMouseListener = new MouseAdapter() {
366                int activeColumn = -1; //no active column yet (prevent table from being reverse ordered)
367                public void mouseClicked(MouseEvent e) {
368                    TableColumnModel columnModel = tableView.getColumnModel();
369                    int viewColumn = columnModel.getColumnIndexAtX(e.getX());
370                    int column = tableView.convertColumnIndexToModel(viewColumn);
371                    //initialize ascending everytime a different column is chosen
372                    if (column != activeColumn) {
373                        ascending = false;
374                        activeColumn = column;
375                    }
376                    if (e.getClickCount() == 1 && column != -1) {
377                        // check TED if sorting by this column is allowed
378                        if (ted.canSortByColumn(column)) {
379                            actualColumn = column;
380                            ascending = !ascending;
381                            ao[0] = new Integer(column);
382                            ao[1] = new Boolean(ascending);
383                            sorter.sortByColumn(actualColumn, ascending);
384                            // Fire Tableheader changed
385                            tableView.getTableHeader().resizeAndRepaint();
386                        }
387                    }
388                }
389            };
390            JTableHeader th = tableView.getTableHeader();
391            th.addMouseListener(listMouseListener);
392        }
393    }