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