001/* ----------------------------------------------------------------------------
002   The Kiwi Toolkit - A Java Class Library
003   Copyright (C) 1998-2004 Mark A. Lindner
004
005   This library is free software; you can redistribute it and/or
006   modify it under the terms of the GNU General Public License as
007   published by the Free Software Foundation; either version 2 of the
008   License, or (at your option) any later version.
009
010   This library is distributed in the hope that it will be useful,
011   but WITHOUT ANY WARRANTY; without even the implied warranty of
012   MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
013   General Public License for more details.
014
015   You should have received a copy of the GNU General Public License
016   along with this library; if not, write to the Free Software
017   Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA
018   02111-1307, USA.
019 
020   The author may be contacted at: mark_a_lindner@yahoo.com
021   ----------------------------------------------------------------------------
022   $Log: TableSorter.java,v $
023   Revision 1.12  2004/05/31 07:29:26  markl
024   improved sorting capability
025
026   Revision 1.11  2004/03/16 06:43:39  markl
027   LocaleManager method change
028
029   Revision 1.10  2003/01/19 09:32:30  markl
030   Modifications to allow toggle sort (first click causes ascending sort, and
031   second click to same column causes descending sort, etc.).
032
033   Revision 1.9  2001/03/20 00:54:56  markl
034   Fixed deprecated calls.
035
036   Revision 1.8  2001/03/12 04:54:32  markl
037   Source code cleanup.
038
039   Revision 1.7  2000/06/09 01:48:29  markl
040   Modified to use Collator to compare strings.
041
042   Revision 1.6  1999/07/25 13:41:26  markl
043   Minor internal fixes.
044
045   Revision 1.5  1999/07/16 07:11:35  markl
046   Added support for Holder objects.
047
048   Revision 1.4  1999/07/05 08:07:15  markl
049   Added logic to maintain the set of highlighted items when sorting is
050   performed, and disabled sorting if the table is not enabled.
051
052   Revision 1.3  1999/05/05 06:39:24  markl
053   Added logic to sync the highlighted rows with the sort.
054
055   Revision 1.2  1999/01/10 03:12:19  markl
056   added GPL header & RCS tag
057   ----------------------------------------------------------------------------
058*/
059
060/*
061 * @(#)TableSorter.java 1.5 97/12/17
062 *
063 * Copyright (c) 1997 Sun Microsystems, Inc. All Rights Reserved.
064 *
065 * This software is the confidential and proprietary information of Sun
066 * Microsystems, Inc. ("Confidential Information").  You shall not
067 * disclose such Confidential Information and shall use it only in
068 * accordance with the terms of the license agreement you entered into
069 * with Sun.
070 *
071 * SUN MAKES NO REPRESENTATIONS OR WARRANTIES ABOUT THE SUITABILITY OF THE
072 * SOFTWARE, EITHER EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE
073 * IMPLIED WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR
074 * PURPOSE, OR NON-INFRINGEMENT. SUN SHALL NOT BE LIABLE FOR ANY DAMAGES
075 * SUFFERED BY LICENSEE AS A RESULT OF USING, MODIFYING OR DISTRIBUTING
076 * THIS SOFTWARE OR ITS DERIVATIVES.
077 *
078 */
079
080package kiwi.ui.model;
081
082import java.awt.event.*;
083import java.text.*;
084import java.util.*;
085import javax.swing.JTable;
086import javax.swing.event.TableModelEvent;
087import javax.swing.table.*;
088
089import kiwi.util.*;
090
091/**
092  * A sorter for <code>TableModel</code>s. The sorter has a model (conforming
093  * to <code>TableModel</code>) and itself implements <code>TableModel</code>.
094  * <code>TableSorter</code> does not store or copy the data in the
095  * <code>TableModel</code>; instead it maintains an array of integers which it
096  * keeps the same size as the number of rows in its model. When the model
097  * changes it notifies the sorter that something has changed eg. "rowsAdded"
098  * so that its internal array of integers can be reallocated. As requests are
099  * made of the sorter (like <code>getValueAt(row, col)</code>) it redirects
100  * them to its model via the mapping array. That way the
101  * <code>TableSorter</code> appears to hold another copy of the table with
102  * the rows in a different order. The sorting algorthm used is stable which
103  * means that it does not move around rows when its comparison function
104  * returns 0 to denote that they are equivalent.
105  *
106  * @author Philip Milne
107  * @author Mark Lindner
108  */
109
110public class TableSorter extends ProxyTableModel
111  {
112  private int indexes[];
113  private Vector sortingColumns = new Vector();
114  private boolean ascending = true;
115  private int compares;
116  private JTable tableView;
117  private int sortedColumn = -1;
118  private boolean sortedAscending = true;
119
120  /** Construct a new <code>TableSorter</code>. */
121
122  public TableSorter()
123    {
124    indexes = new int[0]; // For consistency.
125    }
126
127  /** Construct a new <code>TableSorter</code> with a specified data model..
128    *
129    * @param model The <code>TableModel</code> to use.
130    */
131
132  public TableSorter(TableModel model)
133    {
134    setModel(model);
135    }
136
137  /** Return the index of the given row in the <i>unsorted</i> model that this
138    * model wraps. This method is useful for determining which row in the
139    * actual model is mapped to the currently selected row in the sorted
140    * model.
141    *
142    * @see #getReverseRowTranslation
143    */
144
145  public int getRowTranslation(int row)
146    {
147    return(indexes[row]);
148    }
149
150  /** Return the visible index of the given row in the <i>unsorted</i> model.
151   * This method performs the reverse of <code>getRowTranslation()</code>
152   *
153   * @see #getRowTranslation
154   */
155  
156  public int getReverseRowTranslation(int row)
157    {
158    for(int i = 0; i < indexes.length; i++)
159      if(indexes[i] == row)
160        return(i);
161
162    return(-1);
163    }
164
165  /** Set the <code>TableModel</code> for this table sorter.
166    *
167    * @param model The <code>TableModel</code> to use.
168    */
169
170  public void setModel(TableModel model)
171    {
172    super.setModel(model);
173    reallocateIndexes();
174    }
175
176  /** Get the value at the given row and column of the unsorted table.
177    *
178    * @param row The row.
179    * @param col The column.
180    *
181    * @return The object at the given position in the <i>unsorted</i> model.
182    */
183
184  public Object getValueAt(int row, int col)
185    {
186    // The mapping only affects the contents of the data rows.
187    // Pass all requests to these rows through the mapping array: "indexes".
188
189    checkModel();
190    return model.getValueAt(indexes[row], col);
191    }
192
193  /** Set the value at the given row and column of the unsorted table.
194    *
195    * @param row The row.
196    * @param col The column.
197    * @param value The new value.
198    *
199    */
200
201  public void setValueAt(Object value, int row, int col)
202    {
203    checkModel();
204    model.setValueAt(value, indexes[row], col);
205    }
206
207  /** Sort a column in the table in ascending order.
208    *
209    * @param column The index of the column to sort.
210    */
211
212  public void sortByColumn(int column)
213    {
214    boolean asc = true;
215    
216    // System.out.println("sorting by: " + column + ", and last time col was: " + sortedColumn);
217    
218    if(column == sortedColumn)
219      {
220      // System.out.println("asc is now: " + (! sortedAscending));
221      asc = ! sortedAscending;
222      }
223    
224    sortByColumn(column, asc);
225    }
226
227  /** Sort a column in the table.
228    *
229    * @param column The index of the column to sort.
230    * @param ascending If <code>true</code>, sorts in ascending order;
231    * otherwise, sorts in descending order.
232    */
233
234  public void sortByColumn(int column, boolean ascending)
235    {
236    boolean bigTable = (getRowCount() > 10000);
237
238    if(bigTable)
239      KiwiUtils.busyOn(tableView);
240    
241    this.sortedColumn = column;
242    this.sortedAscending = ascending;
243    this.ascending = ascending;
244    sortingColumns.removeAllElements();
245    sortingColumns.addElement(new Integer(column));
246    sort(this);
247
248    super.tableChanged(new TableModelEvent(this));
249
250    if(bigTable)
251      KiwiUtils.busyOff(tableView);
252    }
253
254  /** Get the index of the last column that the table was sorted on.
255   *
256   * @return The column index, or -1 if there was no previous sort.
257   *
258   * @since Kiwi 1.4
259   */
260
261  public int getSortedColumn()
262    {
263    return(sortedColumn);
264    }
265
266  /** Determine if the last column sort was ascending or descending.
267   *
268   * @return <code>true</code> if the sort was ascending, and
269   * <code>false</code> otherwise. This value is only meaningful if
270   * <code>getSortedColumn()</code> returns a non-negative value.
271   *
272   * @since Kiwi 1.4
273   */
274
275  public boolean isSortedAscending()
276    {
277    return(sortedAscending);
278    }
279  
280  /** Handle <i>table changed</i> events. */
281
282  public void tableChanged(TableModelEvent e)
283    {
284    // System.out.println("Sorter: tableChanged");
285
286    reallocateIndexes();
287    super.tableChanged(e);
288    }
289
290  /** Add a mouse listener to the <code>JTable</code> to trigger a table sort
291    * when a column heading is clicked. A shift click causes the column to
292    * be sorted in descending order, whereas a simple click causes the column
293    * to be sorted in ascending order.
294    *
295    * @param table The <code>JTable</code> to listen for events on.
296    */
297
298  public void registerTableHeaderListener(JTable table)
299    {
300    final TableSorter sorter = this;
301    tableView = table;
302
303    tableView.setColumnSelectionAllowed(false);
304    MouseAdapter listMouseListener = new MouseAdapter()
305      {
306      public void mouseClicked(MouseEvent e)
307        {
308        // If the table is disabled, we will ignore the mouse event,
309        // effectively preventing sorting.
310        
311        if(!tableView.isEnabled() || tableView.isEditing())
312          return;
313        
314        // Save a list of highlighted rows. I don't call getSelectedRows()
315        // directly since that would force me to have *two* working arrays.
316
317        int hrows[] = new int[tableView.getSelectedRowCount()];
318        int nr = tableView.getRowCount();
319        for(int c = 0, i = 0; i < nr ; i++)
320          if(tableView.isRowSelected(i))
321            hrows[c++] = getRowTranslation(i);
322
323        // figure out which column was selected, & sort it
324        
325        TableColumnModel columnModel = tableView.getColumnModel();
326        int viewColumn = columnModel.getColumnIndexAtX(e.getX());
327        int column = tableView.convertColumnIndexToModel(viewColumn);
328        if(e.getClickCount() == 1 && column != -1)
329          {
330          /* int shiftPressed = e.getModifiers() & InputEvent.SHIFT_MASK;
331             boolean ascending = (shiftPressed == 0);
332             sorter.sortByColumn(column, ascending); */
333
334          sorter.sortByColumn(column);
335          }
336
337        // now rehighlight the rows in their new positions
338
339        tableView.clearSelection();
340        for(int i = 0; i < hrows.length; i++)
341          {
342          int r = getReverseRowTranslation(hrows[i]);
343          tableView.addRowSelectionInterval(r, r);
344          }
345        
346        }
347      };
348
349    JTableHeader th = tableView.getTableHeader();
350    th.addMouseListener(listMouseListener);
351    }
352
353  /* internal code follows */
354
355  private int compareRowsByColumn(int row1, int row2, int column)
356    {
357    Class type = model.getColumnClass(column);
358    Collator collator = LocaleManager.getDefault().getCollator();
359
360    TableModel data = model;
361
362    // Check for nulls
363
364    Object o1 = data.getValueAt(row1, column);
365    Object o2 = data.getValueAt(row2, column);
366
367    // If both values are null return 0
368
369    if((o1 == null) && (o2 == null))
370      {
371      return(0);
372      }
373    else if(o1 == null)
374      {
375      // Define null less than everything.
376      return(-1);
377      }
378    else if(o2 == null)
379      {
380      return(1);
381      }
382
383    /* We copy all returned values from the getValue call in case an optimised
384     * model is reusing one object to return many values. The Number
385     * subclasses in the JDK are immutable and so will not be used in this way
386     * but other subclasses of Number might want to do this to save space and
387     * avoid unnecessary heap allocation.
388     */
389
390    if(type.getSuperclass() == Number.class)
391      {
392      Number n1 = (Number)o1;
393      double d1 = n1.doubleValue();
394      Number n2 = (Number)o2;
395      double d2 = n2.doubleValue();
396
397      return((d1 < d2) ? -1 : ((d1 > d2) ? 1 : 0));
398      }
399    else if((type.getSuperclass() == ValueHolder.class)
400            || (type.getClass() == Date.class)
401            || (type.getClass() == String.class))
402      {
403      Comparable c1 = (Comparable)o1;
404      Comparable c2 = (Comparable)o2;
405
406      return(((Comparable)c1).compareTo(c2));
407      }
408    else if(type == Boolean.class)
409      {
410      Boolean bb1 = (Boolean)o1;
411      Boolean bb2 = (Boolean)o2;
412
413      boolean b1 = bb1.booleanValue();
414      boolean b2 = bb2.booleanValue();
415      
416      return(b1 == b2 ? 0 : (b1 ? 1 : -1));
417      }
418    else
419      {
420      if(o1 instanceof Comparable)
421        return(((Comparable)o1).compareTo(o2));
422      else
423        {
424        String s1 = o1.toString();
425        String s2 = o2.toString();
426
427        return(s1.compareTo(s2));
428        }
429      }
430    }
431
432  private int compare(int row1, int row2)
433    {
434    compares++;
435    for(int level = 0; level < sortingColumns.size(); level++)
436      {
437      Integer column = (Integer)sortingColumns.elementAt(level);
438      int result = compareRowsByColumn(row1, row2, column.intValue());
439      if(result != 0)
440        return ascending ? result : -result;
441      }
442    return 0;
443    }
444
445  /*
446   */
447  
448  private void reallocateIndexes()
449    {
450    int rowCount = model.getRowCount();
451
452    // Set up a new array of indexes with the right number of elements
453    // for the new data model.
454
455    indexes = new int[rowCount];
456
457    // Initialise with the identity mapping.
458
459    for(int row = 0; row < rowCount; row++)
460      indexes[row] = row;
461    }
462
463  /*
464   */
465
466  private void checkModel()
467    {
468    if(indexes.length != model.getRowCount())
469      {
470      System.err.println("Sorter not informed of a change in model.");
471      }
472    }
473
474  /*
475   */
476
477  private void sort(Object sender)
478    {
479    checkModel();
480
481    compares = 0;
482
483    shuttlesort((int[])indexes.clone(), indexes, 0, indexes.length);
484    }
485
486  /*
487  public void n2sort()
488    {
489    for(int i = 0; i < getRowCount(); i++) {
490            for(int j = i+1; j < getRowCount(); j++) {
491                if (compare(indexes[i], indexes[j]) == -1) {
492                    swap(i, j);
493                }
494            }
495        }
496    }
497    */
498
499  // This is a home-grown implementation which we have not had time to research
500  // - it may perform poorly in some circumstances. It requires twice the space
501  // of an in-place algorithm and makes NlogN assigments shuttling the values
502  // between the two arrays. The number of compares appears to vary between N-1
503  // and NlogN depending on the initial order but the main reason for using it
504  // here is that, unlike qsort, it is stable.
505
506  private void shuttlesort(int from[], int to[], int low, int high)
507    {
508    if (high - low < 2)
509      {
510      return;
511      }
512
513    int middle = (low + high)/2;
514    shuttlesort(to, from, low, middle);
515    shuttlesort(to, from, middle, high);
516
517    int p = low;
518    int q = middle;
519
520    /* This is an optional short-cut; at each recursive call, check to see if
521     * the elements in this subset are already ordered.  If so, no further
522     * comparisons are needed; the sub-array can just be copied.  The array
523     * must be copied rather than assigned otherwise sister calls in the
524     * recursion might get out of sinc.  When the number of elements is three
525     * they are partitioned so that the first set, [low, mid), has one element
526     * and the second, [mid, high), has two. We skip the optimisation when the
527     * number of elements is three or less as the first compare in the normal
528     * merge will produce the same sequence of steps. This optimisation seems
529     * to be worthwhile for partially ordered lists but some analysis is
530     * needed to find out how the performance drops to Nlog(N) as the initial
531     * order diminishes - it may drop very quickly.
532     */
533
534    if(high - low >= 4 && compare(from[middle-1], from[middle]) <= 0)
535      {
536      for(int i = low; i < high; i++)
537        {
538        to[i] = from[i];
539        }
540      return;
541      }
542
543    // A normal merge.
544
545    for(int i = low; i < high; i++)
546      {
547      if(q >= high || (p < middle && compare(from[p], from[q]) <= 0))
548        {
549        to[i] = from[p++];
550        }
551      else
552        {
553        to[i] = from[q++];
554        }
555      }
556    }
557
558  /*
559   */
560  
561  private void swap(int i, int j)
562    {
563    int tmp = indexes[i];
564    indexes[i] = indexes[j];
565    indexes[j] = tmp;
566    }
567
568  }
569
570/* end of source file */