Home | Contact Us | FAQ | Search & Site Map | Link to Us
Sign In | Join | Other 45 Sites in Network
HomeAnnouncementsWhite Papers
Discussion GroupsFirst AidDatabasesJavaBeansGUIJava 3DVirtual MachineCORBASecurityToolsGeneral
Java DirectoryOpen Source ProjectsSample Book ChaptersUser GroupsWeb Resources
Related Topics
Databases.NETMore Topics ...

Java Forum / General / October 2005

Tip: Looking for answers? Try searching our database.

resize JTable

Thread view: 
mmellinger66@gmail.com - 26 Sep 2005 23:27 GMT
I'm trying to dynamically resize the number of rows in my JTable
object.  Basically, I'm reading data from a database table and I'm
allowing people to filter to return a subset of the data.  I could set
the size to the approximate maximum, but when the user filters, there
may only be 5 results out of 1000 returned.  The scrollbar will be too
small.  In addition, when I try to print() with the 1.5 JTable print,
it will send too many pages to the printer.  I see where I can create a
data model, etc. but I have half a dozen little screens where I want to
provide this functionality.  Seems like a lot of work...

-Mike
zero - 27 Sep 2005 00:20 GMT
> I'm trying to dynamically resize the number of rows in my JTable
> object.  Basically, I'm reading data from a database table and I'm
[quoted text clipped - 7 lines]
>
> -Mike

Changing the number of rows shouldn't be any problem at all.  You just
change the underlaying TableModel, and notify the JTable component of this
(I believe that's a fireTableModelChanged).  And remember this is OO, so
you only have to have one TableModel for all your screens, and dynamically
fill in the data.  Keep the GUI and the model separate.

Beyond that I'm afraid there one big truth you'll have to face: JTables are
very powerful, but if you want anything but the most basic functionality,
it'll be a lot of work.
Roedy Green - 27 Sep 2005 01:34 GMT
>I'm trying to dynamically resize the number of rows in my JTable
>object.  Basically, I'm reading data from a database table and I'm
[quoted text clipped - 5 lines]
>data model, etc. but I have half a dozen little screens where I want to
>provide this functionality.  Seems like a lot of work...

I have read your request several times and I am still not sure what
you problem is.

Why not change your table and only tell swing about it with your event
firings  only when you know how many rows you got?  You could  keep a
copy of the old model around until the new one is complete, then swap
it in as a whole.
Signature

Canadian Mind Products, Roedy Green.
http://mindprod.com Again taking new Java programming contracts.

mmellinger66@gmail.com - 27 Sep 2005 03:11 GMT
I don't have a model.  I figure out how many rows(max) of data that I
will need then I create the JTable with the appropriate number of rows
then change them as I load in the new dataset.  I want to filter the
data based on 1 or 2 criteria to get a subset to reduce the results
display in the table from 200, for example, to 5 then allow someone to
print the table.  It's all pretty simple except that when I go to
print, I get all those blank rows because I initialized the JTable with
a size of 250.  It almost works without adding the model.  I've got
half a dozen screens that I want like this.

Anyway, I guess I need to build a model and maintain it myself.   I've
done a lot of Tcl/Tk and I'm new to Java so I guess I'm used to doing
this the "easy" way.  The results are impressive so far, so I'll get
over the little bit of extra it.  Java has come a long way...

-Mike
Dag Sunde - 27 Sep 2005 06:45 GMT
>I don't have a model.  I figure out how many rows(max) of data that I
> will need then I create the JTable with the appropriate number of rows
[quoted text clipped - 10 lines]
> this the "easy" way.  The results are impressive so far, so I'll get
> over the little bit of extra it.  Java has come a long way...

As Zero said in an earlier post, There is almost nothing you can't do
with JTable, but for anything than the most basic you'll need a data-
model.

Put some work into the design of your model and rely on the events
the JTable listens to, and complex tables becomes a lot easier.

You do all and any work on the data in the datamodel itself, and
just tell the table when you're done.

Signature

Dag.

Roedy Green - 27 Sep 2005 07:23 GMT
>but for anything than the most basic you'll need a data-
>model.

Even when you don't have a DataModel, I gather what happens is JTable
constructs a simple one for you.

Some day I will have to look and see what JTable does. It feels like
just a skeleton to hang your own code on.
Signature

Canadian Mind Products, Roedy Green.
http://mindprod.com Again taking new Java programming contracts.

Dag Sunde - 27 Sep 2005 10:30 GMT
>>but for anything than the most basic you'll need a data-
>>model.
[quoted text clipped - 4 lines]
> Some day I will have to look and see what JTable does. It feels like
> just a skeleton to hang your own code on.

I read somwhere the other day that JTable at first came without the
constructor that let you define the data on instantiation of the JTable.

But people where complaining about how difficult it was to create a
datamodel just to show some simple tabular data, so they they added a
couple of convenience constructors that did the work behind the sceenes.

Later on the designers at Sun regretted this painfully, because of the
confusion and misunderstandings it have created.

Signature

Dag.

steve - 01 Oct 2005 23:10 GMT
>>> but for anything than the most basic you'll need a data-
>>> model.
[quoted text clipped - 14 lines]
> Later on the designers at Sun regretted this painfully, because of the
> confusion and misunderstandings it have created.

yes they messed it up badly. the other thing they messed up was not being
able to easily sort the data , from a user point of view.
you have to resort to all sorts of tricks , ESP. if the user can add & delete
lines from the table. ( whilst in sort mode)
Roedy Green - 02 Oct 2005 03:21 GMT
>yes they messed it up badly. the other thing they messed up was not being
>able to easily sort the data , from a user point of view.
>you have to resort to all sorts of tricks , ESP. if the user can add & delete
>lines from the table. ( whilst in sort mode)

here is the song and dance I had to go through to keep a table ordered
with new elements arriving with random sort keys to insert them in the
right spot or overwrite an existing element.

package com.mindprod.table;

import java.awt.event.InputEvent;
import java.awt.event.MouseAdapter;
import java.awt.event.MouseEvent;
import java.util.Comparator;
import java.util.HashMap;

import javax.swing.JTable;
import javax.swing.table.JTableHeader;
import javax.swing.table.TableColumnModel;

/**
* TableOrderer, Keeps a table in order sorted on several columns.
* Also ensures no dups inserted.
* Also ensures table does not get too big.
* Does not implement OrderedTableModel but requires a TableModel that
does.
* It is not aware of how long the various rows are.
* It just keeps track of rows.
*
* @author Roedy Green
* @version 1.1
* @since 2002  Sep 10
*/
public class TableOrderer
  {

  /**
   * Column sorted in ascending order.
   */
  public static final int ASCENDING = 1;

  /**
   * Column sorted in descending order
   */
  public static final int DESCENDING = -1;

  /**
   * Column not sorted.
   */
  public static final int UNSORTED = 0;

  /**
  * constant to specify pruning tables at row 0,
  * Usually used with ascending sorts. Scrolling text moves up.
  * Chops off biggest values.
  */
  public final static int CHOP_AT_TOP = 0;

  /**
   * constant to specify pruning tables at the last row.
   * Usually used with descending sorts, or unsorted
   * tables. Scrolling text moves down. chops off smallest values.
   */
  public final static int CHOP_AT_BOTTOM = 1;

  /**
   * whether ith key to sort on is ascending or
   * descending.  True for ascending.
   */
  private boolean[] ascDesc;

  /**
   *comparator to order this table, null forunsorted.
   */
  private Comparator comparator;

  /**
   * Column we want to keep free of duplicates, 0-based.
   */
  private int deDupCol;

  /**
   * List of keys we don't want to duplicate
   * and where they currently are in the list.
   */
  private HashMap deDupMap = null;

  /**
   * true if the deduping code is turned on.
   */
  private boolean deDupEngaged;

  /**
   * Maximum number of rows we will allow the table to grow too.
   * We let the table temporarily get one bigger than this.
   */
  private int maxRows = 1000;

  /**
   * OrderedTableModel that we are keeping in
   * order and deduped.
   */
  private OrderedTableModel model;

  /**
   * most recently changed row.  If rows are added, this does not
slide.
   * It is the absolute position of the data when it was recently
changed,
   * not where it slid to now.
   */
  private int mostRecentlyChangedRow = -1;

  /**
   * CHOP_AT_TOP CHOP_AT_BOTTOM
   */
  private int whereToChop = CHOP_AT_TOP;

  /**
  * zero based columns to sort on, major first.
  * null if no sort keys. Usually array of one element holding the
0-based column to sort on.
  */
  private int[] sortCols0Based;

  /**
   * true if the sorting code is turned on.
   */
  private boolean sortEngaged;

  /**
   * Track how long (in looks) the dup list has remained stable.
   * If it sits stable it is faster to use the HashMap,
   * if not, linear search for detecting dups.
   */
  private int stableFor = 0;

  /**
   * Public constructor.
   * Should do a setComparator, setMaxRows, setWhereToChop,
setDeDupCol as well.
   *
   * @param model  OrderedTableModel to be kept deduped/sorted.
   */
  public TableOrderer ( OrderedTableModel model )
     {
     this.model = model;
     }

  /**
   * replace a row cyclically within an single table.
   * Does no sort or dedup.
   * @param aux     auxiliary data about the row,
   *                usually dangerlevel and age.
   *                possibly null.
   *
   * @param rowData row of Object data to add.
   */
  public void acceptCyclicRow( Object[] rowData, Object aux )
     {
     int row = mostRecentlyChangedRow+1;
     if ( row >= maxRows )
        {
        row  = 0;
        }
     if ( row < model.getRowCount() )
        {
        // write over top of an existing row.
        model.replaceRow( rowData, aux, row );
        setMostRecentlyChangedRow( row );
        model.fireTableRowsUpdated( row, row );
        }
     else
        {
        // it is a brand new row, add it to the end, will not
overflow.
        model.addRow( rowData, aux, row );
        setMostRecentlyChangedRow( row );
        model.fireTableRowsInserted( row, row );
        }
     }

  /**
   * Accept a new row, add/delete/replace existing row if duplicate.
   *
   * @param rowData row of Object data to add.
   * @param aux     auxiliary data about the row,
   *                usually dangerlevel and age.
   *                possibly null.
   * @param specialInstructions
   *                ServerDataItemResponse.ADD, ADD_NO_DEDUP or
DELETE.
   */
  public void acceptRow( Object[] rowData, Object aux, int
specialInstructions )
     {
     switch ( specialInstructions )
        {
        case ServerDataItemResponse.ADD:
           this.addRow( rowData, aux);
           break;

        case ServerDataItemResponse.ADD_NO_DEDUP:
           this.addRowInOrder ( rowData, aux );
           break;

        case ServerDataItemResponse.DELETE:
           this.deleteMatchingRow ( rowData );
           break;

        default:
           Log.println( Log.BUG,"invalid specialInstructions.");
        }
     }

  /**
   * Add a new row, replace existing row if duplicate.
   *
   * @param rowData row of Object data to add.
   * @param aux     auxiliary data about the row,
   *                usually dangerlevel and age.
   *                possibly null.
   */
  public void addRow( Object[] rowData, Object aux )
     {
     if ( deDupEngaged )
        {
        int row = findDup( rowData[ deDupCol ] );
        if ( row >= 0 )
           {
           // was a duplicate, replace existing
           // This should be the normal case.
           this.replaceRow ( rowData, aux, row );
           }
        else
           {
           /* not a duplicate */
           this.addRowInOrder ( rowData, aux );
           /* deDupMap is no longer valid, we don't know where that
row will go or how it will shift other rows. */
           invalidateDeDupMap();
           }
        }
     else
        {
        /* no deduping */
        this.addRowInOrder ( rowData, aux );
        }
     }

  /**
   * add a new row wherever it belongs in sort order.
   *
   * @param rowData row of Object data to add.
   * @param aux     auxiliary data about the row,
   *                usually dangerlevel and age.
   *                possibly null.
   */
  private void addRowInOrder( Object[] rowData, Object aux)
     {
     if ( sortEngaged )
        {
        /* row where found dup */
        int where = model.binarySearch( rowData, comparator );
        /* row where to insert new row */
        int insertAt = where;
        if ( where < 0 )
           {
           /* we did not find it there already,
           where contains (-(insertion point) - 1) */
           insertAt = -where - 1;
           }
        else
           {
           /* found key, we have the index of one of the dups. */
           switch ( whereToChop )
              {
              case CHOP_AT_BOTTOM:

                 // We chop at bottom, (desc order) so we want to add
before all dups.
                 // Remember that descending Comparator has reverse
sign result.
                 for ( int row=where-1; row>=0; row-- )
                    {
                    if ( comparator.compare( rowData,
model.getQuickRowData(row)) > 0 )
                       {
                       insertAt = row+1;
                       break;
                       }
                    }
                 if ( insertAt == where )
                    {
                    // did not find anything smaller
                    insertAt = 0;
                    }
                 break;

              case CHOP_AT_TOP:

                 // We chop at top, (asc order) so we want to add
after all dups.
                 // CHOP_AT_TOP
                 int numRows = model.getRowCount();
                 for ( int row=where+1; row<numRows; row++ )
                    {
                    if ( comparator.compare(
model.getQuickRowData(row), rowData) > 0 )
                       {
                       insertAt = row;
                       break;
                       }
                    }
                 if ( insertAt == where )
                    {
                    // did not find anything bigger
                    insertAt = numRows;
                    }
                 break;

              default: break;
              } // end switch
           } // end else
        // We now have the insertion point.
        /* add either at insertion point or or after dup */
        if ( insertAt >= maxRows && whereToChop == CHOP_AT_BOTTOM )
           {
           // off the bottom end.  No point in inserting at all, will
just get chopped.
           insertAt = -1;
           }

        if ( insertAt == 0 && model.getRowCount() >= maxRows &&
whereToChop == CHOP_AT_TOP )
           {
           // off the top end. table is full.  Adding a row at the
top will just get chopped.
           // don't bother.
           insertAt = -1;
           }

        if ( insertAt >= 0 )
           {

           model.addRow( rowData, aux, insertAt );
           setMostRecentlyChangedRow( insertAt );
           model.fireTableRowsInserted( insertAt, insertAt );
           /* inserted record may push existing one off either end */
           chopTable( 0 );
           }
        // otherwise we discarded the row as off either end.
        // leave existing mostRecentlyChangedRow
        }
     else
        {
        // not sorting    
        // make room for new row
        chopTable( 1 );
        switch ( whereToChop )
           {
           case CHOP_AT_BOTTOM:
              // not sorting, just add at the top,
              model.addRow( rowData, aux, 0 );
              setMostRecentlyChangedRow ( 0 );
              model.fireTableRowsInserted( 0, 0 );
              break;

           case CHOP_AT_TOP:
              // not sorting, just add at the bottom
              int size = model.getRowCount();
              model.addRow( rowData, aux, size );
              setMostRecentlyChangedRow( size );
              model.fireTableRowsInserted( size, size );
              break;
           }
        }
     invalidateDeDupMap(); /* added row, disturbs map */
     }

  /**
   * Allow user to select a new sort column.
   * Works by adding a MouseListener to the column headers.
   * Mouse listener fields requests for sorting columns.
   * Allows click, shift-click on column to select column and
ascending/descending.
   *
   * @param table  the visible JTable part of the table.
   */
  public void allowUserToSelectSortColumn( JTable table )
     {
     // make available to anonymous inner class
     final JTable tableView = table;
     tableView.setColumnSelectionAllowed( false );
     JTableHeader th = tableView.getTableHeader();
     th.addMouseListener(
                        new MouseAdapter()
                           {
                           /**
                            * Handler for mouse selection of sort
column
                            * and order. Shift-click means
descending.
                            *
                            * @param event  Details of event
                            */
                           public void mouseClicked( MouseEvent event
)
                              {
                              TableColumnModel columnModel =
tableView.getColumnModel();
                              int viewColumn =
columnModel.getColumnIndexAtX( event.getX() );
                              int column =
tableView.convertColumnIndexToModel( viewColumn );
                              if ( event.getClickCount() == 1 &&
column != -1 )
                                 {
                                 int shiftPressed =
event.getModifiers() & InputEvent.SHIFT_MASK;
                                 boolean ascending = ( shiftPressed
== 0 );
                                 // if we are ascending, we chop at
the top, if descending at the bottom.
                                 TableOrderer.this.setWhereToChop(
ascending ?
TableOrderer.CHOP_AT_TOP :
TableOrderer.CHOP_AT_BOTTOM);
                                 // uses 1-based column numbers, -ve
to request descending
                                 TableOrderer.this.sortByColumns( new
int[] { ascending ? column +1 : -(column+1)} );
                                 }
                              }
                           }
                        );
     } // end allowUserToSelectSortColumn

  /**
   * Are two rows exactly equal? (except aux).
   *
   * @param rowa   First row to compare. Defines number of columns.
   * @param rowb   Second row To Compare
   *
   * @return true if all fields exactly match.
   */
  private static boolean areRowsExactlyEqual ( Object[] rowa,
Object[] rowb )
     {
     int cols = rowa.length;
     // rowb may have an aux appended, one extra ignored column.
     for ( int i=0; i<cols; i++ )
        {
        if ( ( (Comparable)rowa[i] ).compareTo( rowb[i] ) != 0 )
           {
           return false;
           }
        }
     return true;
     }

  /**
   * chop table back down to size if it has grown
   * too big. We do this whether or not deDuping or sorting.
   * Chop from bottom or top as controlled by whereToChop.
   * ChopTable maintains mostRecentlyChangedRow.
   *
   * @param room   How many free slots are needed to be free, in
rows.
   */
  private void chopTable( int room )
     {
     // remove excess rows, working backwards.  More efficient.
     // In practice should never have to chop more than one row,
     // so it does not may to save up the fireTableRows events.
     int highestRowToDelete = model.getRowCount()-1;
     int highestRowToKeep = maxRows-1-room;
     for ( int i=highestRowToDelete; i>highestRowToKeep; i-- )
        {
        switch ( whereToChop )
           {
           
           case CHOP_AT_BOTTOM:
              model.removeRow ( i );
              model.fireTableRowsDeleted(i, i);
              // don't setMostRecentlyChangedRow yet
              break;

           case CHOP_AT_TOP:
              model.removeRow ( 0 );
              model.fireTableRowsDeleted(0, 0);
              // don't setMostRecentlyChangedRow yet
              break;

           default:
              throw new IllegalArgumentException
("TableOrderer.chopTable expects only CHOP_AT_BOTTOM or CHOP_AT_TOP");
           }
        /* deDupMap is no longer valid, since chopped key is no
longer in list. */
        /* only called if we chop a row */
        invalidateDeDupMap();
        } // end for
     }

  /**
   * find a duplicate via a dedup mechanism. Needs match on only one
column.
   * presumes deDupEngaged.
   *
   * @param item   Item to check for duplicate. The value of the
   *               cell to match in the deDup column.
   * @return row where dup found, or -1 if not a dup.
   */
  private int findDup ( Object item )
     {
     int row;
     /* check to see if we have seen this key before */
     /* pick faster algorithm, depending on recent stability. */
     if ( stableFor > 50 )
        {
        row = findDupViaMap( item );
        }
     else
        {
        /* Everthing is changing so fast, no point in using deDupMap,
since
           it goes out of date and has to be rebuilt. */
        row = findDupViaModel( item );
        }
     stableFor++; /* note one more cycle of stability. */
     return row;
     }

  /**
   * Find duplicate of this row using HashMap lookup.
   * This technique means we don't need to sort on dedup column.
   *
   * @param item   Item to check for duplicate. The value of the
   *               cell to match in the deDup column.
   * @return row where dup found, or -1 if not a dup.
   */
  private int findDupViaMap( Object item )
     {
     if ( deDupMap == null )
        {
        rebuildDeDupMap();
        }
     Integer row = (Integer)deDupMap.get( item );
     return row == null ? -1 : row.intValue();
     }

  /**
   * Find duplicate of this row using linear search of Model
   *
   * @param item   Item to check for duplicate. The value of the
   *               cell to match in the deDup column.
   * @return row where dup found, or -1 if not a dup.
   */
  private int findDupViaModel( Object item )
     {
     int numRows = model.getRowCount();
     for ( int row=0; row<numRows; row++ )
        {
        if ( item.equals( model.getValueAt( row, deDupCol ) ) )
           {
           return row;
           }
        }
     return -1;
     }

  /**
   * Delete a matching row, all fields must match exactly except aux.
   * If more than one row matches, only first match is deleted.
   * If no match in found, that is OK.
   *
   * @param rowData matching row of Object data to delete.
   */
  public void deleteMatchingRow( Object[] rowData )
     {
     // try for a fast lookup via the dedup mechanism.
     int row = -1;
     if ( deDupEngaged )
        {
        row = findExactMatchViaMap( rowData );
        }
     if ( row < 0 )
        {
        // dedup mechanism failed, try linear search from the top
        row = findExactMatchViaModel( rowData, 0 );
        }

     if ( row >= 0 )
        {
        model.removeRow( row );
        /* most recently changed row won't be there, use row where it
was. */
        setMostRecentlyChangedRow( row );
        // if there is a second match, we leave it.
        model.fireTableRowsDeleted( row, row );
        }
     // otherwise could not find a match. Don't complain.
     }  // end deleteMatchingRow

  /**
   * Find exact duplicate of this row using dedup mechanism.
   * all fields must match. Presumes deDupEngaged,
   *
   * @param rowData row of Object data to match
   * @return row where match found, or -1 if not found
   */
  private int findExactMatchViaMap( Object[] rowData )
     {
     int row = findDup( rowData[ deDupCol ] );
     if ( row >= 0 )
        {
        if ( areRowsExactlyEqual( rowData, model.getQuickRowData( row
) ) )
           {
           return row;
           }
        // try linear search of succeeding entries
        return findExactMatchViaModel ( rowData, row+1 );
        }
     return -1;
     }

  /**
   * Find exact duplicate of this row using linear search of Model.
   * All fields must match.
   *
   * @param rowData  row of Object data to match
   * @param startRow row where to start looking.  Does not search
   *                 above this row.
   * @return row where match found, or -1 if not found
   */
  private int findExactMatchViaModel( Object[] rowData, int startRow
)
     {
     // do a linear search starting at the given row. No wrap around.
     int numRows = model.getRowCount();
     for ( int row=startRow; row<numRows; row++ )
        {
        if ( areRowsExactlyEqual ( rowData,  model.getQuickRowData(
row ) ) )
           {
           return row;
           }
        }
     return -1;
     }

  /**
   * Mark deDupMap as no longer accurate.
   * The mapping of dups to row numbers has
   * been disturbed.  The table needs to be
   * rebuilt.
   */
  private void invalidateDeDupMap()
     {
     deDupMap = null;
     stableFor = 0;
     }

  /**
   * Was this row recently changed?
   * If so it will likely get inverse highlighting.
   *
   * @param row    0-based row in question.
   * @return true if this was the most recently painted row.
   */
  public boolean isRecentlyChanged ( int row )
     {
     return row == mostRecentlyChangedRow;
     }

  /**
   * Which row was most recently changed?
   *
   * @return 0-based row most recently changed.
   *         -1 if none.
   */
  public int getMostRecentlyChangedRow ()
     {
     return  mostRecentlyChangedRow;
     }

  /**
   * How is a given column sorted.  Used by header painting routines.
   *
   * @param column zero-based column number
   * @return  1, 0, -1 ASCENDING UNSORTED DESCENDING
   */
  public int howColumnSorted ( int column )
     {
     if ( sortCols0Based == null || sortCols0Based.length == 0 )
return UNSORTED;
     for ( int i=0; i<sortCols0Based.length; i++ )
        {
        if ( column == sortCols0Based[i] ) return  ascDesc[i] ?
ASCENDING : DESCENDING;
        }
     return UNSORTED;
     }

  /**
   * rebuild the list of dups and which row
   * they are associated with.
   * This is likely to be a bottleneck.
   * Becomes invalid as soon as any row moves or changes dedup key.
   * Only useful when display has a fixed set of dedup keys with
changing data.
   */
  private void rebuildDeDupMap ()
     {
     if ( deDupEngaged )
        {
        int numRows = model.getRowCount();
        deDupMap = new HashMap( numRows * 2 );
        for ( int i=0; i<numRows; i++ )
           {
           deDupMap.put( model.getValueAt(i, deDupCol), new
Integer(i));
           }
        }
     }

  /**
   * replace a row, doing sort adjust.
   *
   * @param rowData row of Object data to replace the existing row
with.
   * @param aux     auxiliary data about the row,
   *                usually dangerlevel and age.
   *                possibly null.
   * @param row     row number to replace
   */
  public void replaceRow( Object[] rowData, Object aux, int row )
     {
     if ( sortEngaged )
        {
        model.replaceRow( rowData, aux, row );

        // need to determine if this disturbed the existing ordering.
        if ( ( row > 0 && model.compareRows( row, row-1, comparator )
< 0 )
             ||( row < model.getRowCount()-1 &&
                 model.compareRows( row+1, row, comparator ) < 0 ) )
           {
           // row is now out of place.
           // mostRecentlyChangedRow will soon be set in
addRowInOrder
           model.removeRow( row );
           model.fireTableRowsDeleted( row, row );
           this.addRowInOrder( rowData, aux );
           /* deDupMap is no longer valid, we don't know where that
row will go or how it will shift other rows. */
           invalidateDeDupMap();
           }
        else
           {
           // did not disturb order
           setMostRecentlyChangedRow( row );
           model.fireTableRowsUpdated( row, row );
           }
        }
     else
        {
        // no sort
        model.replaceRow( rowData, aux, row );
        setMostRecentlyChangedRow( row );
        model.fireTableRowsUpdated( row, row );
        }
     } // end replaceRow

  /** which column should be keep free of duplicates?
   *
   * @param deDupCol1based to keep free of duplicates. 1-based.
   *  0 means no deduping
   */
  public void setDeDupColumn( int deDupCol1based )
     {
     int deDupCol = deDupCol1based - 1;
     if ( this.deDupCol != deDupCol )
        {
        this.invalidateDeDupMap();
        }
     this.deDupCol = deDupCol;
     this.deDupEngaged = ( deDupCol >= 0 );
     }

  /**
   * How many rows of data should be allow to accumulate before
pruning it back.
   * Does not clear the model of data.
   * @param maxRows max size we are to allow the table to get.  If it
gets
   * bigger we chop off the last row.
   * 0 means leave the value as it was.
   */
  public void setMaxRows( int maxRows )
     {
     if ( maxRows != 0 )
        {
        this.maxRows = maxRows;
        chopTable( 0 );
        }
     }

  /**
   * Track the MostRecentlyChangedRow and
   * previouslyChangedRow. Used to refresh display
   * of previously changed row.
   *
   * @param mostRecentlyChangedRow
   *               New row that was most recently changed.
   *               -1 if none.
   */
  private void setMostRecentlyChangedRow ( int mostRecentlyChangedRow
)
     {
     int prev = this.mostRecentlyChangedRow;
     this.mostRecentlyChangedRow = mostRecentlyChangedRow;
     if ( 0 <= prev && prev != mostRecentlyChangedRow && prev <
model.getRowCount() )
        {
        /* Repaint the previous, since likely won't need highlight
any more.
           It does not matter if data moved. We want the slot where
it was. */
        model.fireTableRowsUpdated( prev, prev );
        }
     /* don't need to fire new mostRecentlyChangedRow, since handled
separately.
        There are too many variants of the fireTableChange  */
     }

  /**
   * Where should data be pruned when the table gets too big?
   * @param whereToChop CHOP_AT_TOP  if should chop bottom element
when table gets too big,
   *  CHOP_AT_BOTTOM  chop the top element.
   */
  public void setWhereToChop( int whereToChop )
     {
     this.whereToChop = whereToChop;
     chopTable( 0 );
     }

  /**
   * reSort the entire table, using the current Comparator.
   */
  private void sort()
     {
     if ( sortEngaged )
        {
        if ( model.getRowCount() > 0 )
           {
           // will not be able to track where most recently changed
went.
           setMostRecentlyChangedRow( -1 );
           model.sort( comparator );
           model.fireTableDataChanged();

           }
        /* deDupMap is no longer valid, who knows how sort shuffled.
*/
        invalidateDeDupMap();
        }
     } // end sort

  /**
   * Sort on a set of columns
   *
   * @param sortCols1Based
   *               array of column-1 based columns so sort on,
   *               major key first.
   *               Negative column number means desceding sort.
   */
  public void sortByColumns( int[] sortCols1Based )
     {
     final int size = sortCols1Based.length;
     if ( size == 0 )
        {
        this.sortCols0Based = null;
        this.ascDesc = null;
        this.sortEngaged = false;
        this.comparator = null;
        }
     else
        {
        // convert to more convenient
        // zero-based format, with separate ascending/descending
boolean.
        this.sortCols0Based = new int[ size ];
        this.ascDesc = new boolean[ size ];
        for ( int i=0; i<size; i++ )
           {
           if ( sortCols1Based[i] == 0 )
              {
              throw new IllegalArgumentException("columns to sort
cannot be 0 in TableOrderer.sortByColumns");
              }
           sortCols0Based[ i ] = Math.abs( sortCols1Based[ i ] ) -1;
           ascDesc[ i ] = sortCols1Based[ i ] > 0 /* true=ascending
*/;
           }
        this.comparator = new MultiColumnComparator( sortCols0Based,
ascDesc );
        this.sortEngaged = true;
        sort();
        }
     // make sure headers get repainted
     model.fireTableStructureChanged();
     }

  } // end TableOrderer

Signature

Canadian Mind Products, Roedy Green.
http://mindprod.com Again taking new Java programming contracts.

steve - 02 Oct 2005 11:45 GMT
>> yes they messed it up badly. the other thing they messed up was not being
>> able to easily sort the data , from a user point of view.
[quoted text clipped - 5 lines]
> with new elements arriving with random sort keys to insert them in the
> right spot or overwrite an existing element.

<polite snip>

yep thats about the limit of it, no wonder people shy away from tables.
how about deleting elements, and sorting on dates , as a date?

the other thing i hate , and had to write from scratch, was a security
implementer, so that different users only have  access to certain buttons ,
menu items  lists etc, but could share common screens. That's still a work in
progress!!

Steve
Dag Sunde - 02 Oct 2005 18:22 GMT
>>yes they messed it up badly. the other thing they messed up was not being
>>able to easily sort the data , from a user point of view.
[quoted text clipped - 5 lines]
> with new elements arriving with random sort keys to insert them in the
> right spot or overwrite an existing element.

http://java.sun.com/docs/books/tutorial/uiswing/components/table.html#sorting

Have you seen thisone?
One tablemodel for data feeds the second. Second tablemodel feeds
Table, and implements sorting, filtering, etc.

---------    TableModel   ----------  TableModel    --------
|       | <-------------- |        | <------------- |      |
| Model |                 | Sorter |                | View |
|       | --------------> |        | -------------> |      |
--------- TblModelListen  ---------| TblModelListen --------

Signature

Dag.

steve - 02 Oct 2005 23:09 GMT
>>> yes they messed it up badly. the other thing they messed up was not being
>>> able to easily sort the data , from a user point of view.
[quoted text clipped - 17 lines]
>>> --------------> |        | -------------> |      |
> --------- TblModelListen  ---------| TblModelListen --------

yep it's bugged.
but it could be good, as it does not actually sort the table , but keeps an
ordered list of row indexes.
However it can get real messy , if you start adding & removing rows.

steve
Roedy Green - 03 Oct 2005 00:57 GMT
>---------    TableModel   ----------  TableModel    --------
>|       | <-------------- |        | <------------- |      |
>| Model |                 | Sorter |                | View |
>|       | --------------> |        | -------------> |      |
>--------- TblModelListen  ---------| TblModelListen --------

My first reaction -- that's crazy, all the data duplicated, but it is
not as bad as it looks. There in only one set of objects, just the
references are duplicated.

I am puzzled though how having two copies makes things any easier. You
still have to figure out where to insert/delete. It is not as though
you just tack the new elts on the end and call sort every time.
Signature

Canadian Mind Products, Roedy Green.
http://mindprod.com Again taking new Java programming contracts.

steve - 03 Oct 2005 10:40 GMT
>> ---------    TableModel   ----------  TableModel    --------
>>>> <-------------- |        | <------------- |      |
[quoted text clipped - 9 lines]
> still have to figure out where to insert/delete. It is not as though
> you just tack the new elts on the end and call sort every time.

yes , it's not  clean, he just posted part of the  diagram from the sun
website

the data is a single copy, then a "list is maintained", and the rows
translated.

so basically you can go to your table model, ask for row 1 , the sorter then
goes to the list & knows that 'sorted' row 1 is actually  'un-sorted' row 6 ,
so actually returns row 6.

like i said it  can get messy for adding/removing rows., and it is a buggy
implementation, that is not error trapped correctly.
I think sun never meant it to be anything more than  a very basic  sort
routine.

no to mention it often crashes, when the table contains no data.
and the sorter is broken.

ESP. for dates.

24/11/2005
is smaller than
27/06/1999

and we all 'know' why :-)

steve
Rogan Dawes - 03 Oct 2005 11:43 GMT
>>My first reaction -- that's crazy, all the data duplicated, but it is
>>not as bad as it looks. There in only one set of objects, just the
[quoted text clipped - 3 lines]
>>still have to figure out where to insert/delete. It is not as though
>>you just tack the new elts on the end and call sort every time.

The trick is that you DO just add the elements into your original table
model, whereever you want to. The Sorter listens for new rows being
added, and depending on how many are being added, either works out where
to insert the single new row, or else reperforms the sort.

If you provide the user with a way of selecting a row in the table, and
saying "delete" or "insert", the sorter provides a "int modelIndex(int
viewIndex)" method that translates the visible row to the underlying
tablemodel's row, so you can then call
"originaltableModel.removeRow(row)" or whatever . . .

> yes , it's not  clean, he just posted part of the  diagram from the sun
> website

I dunno, I think the concept is pretty clean, personally.

> the data is a single copy, then a "list is maintained", and the rows
> translated.
[quoted text clipped - 5 lines]
> like i said it  can get messy for adding/removing rows., and it is a buggy
> implementation, that is not error trapped correctly.

I may have had to fix a few things, but it is working perfectly for me
now. Get it at
<http://cvs.sourceforge.net/viewcvs.py/owasp/webscarab/src/org/owasp/webscarab/ut
il/swing/TableSorter.java?rev=1.1&view=markup
>

Although the version that I have checked in there is still 1.1, so I
doubt that I have made any changes to it.

> I think sun never meant it to be anything more than  a very basic  sort
> routine.

Maybe we are talking about different versions. The one that I have (from
sun) is quite comprehensive, and allows you to sort on multiple columns,
ascending or descending, etc.

> no to mention it often crashes, when the table contains no data.
> and the sorter is broken.
[quoted text clipped - 6 lines]
>
> and we all 'know' why :-)

Yes, because you are presenting a String version of your date in the
table, rather than a Date object and using a renderer . . . The version
I link to uses Comparable to determine how to sort items.

> steve

Rogan
Dag Sunde - 03 Oct 2005 13:11 GMT
>>>My first reaction -- that's crazy, all the data duplicated, but it is
>>>not as bad as it looks. There in only one set of objects, just the
[quoted text clipped - 57 lines]
> table, rather than a Date object and using a renderer . . . The version I
> link to uses Comparable to determine how to sort items.

Thank you, Rogan...

I was starting to get worried about this, and thought I had recommended
something that was seriously flawed.

I use it a lot myself, and have never had any problems with it.

I never take any special considerations when inserting or deleting
rows, and my dates are dates in my model, so...

Signature

Dag.

steve - 03 Oct 2005 22:22 GMT
>>>> My first reaction -- that's crazy, all the data duplicated, but it is
>>>> not as bad as it looks. There in only one set of objects, just the
[quoted text clipped - 68 lines]
> I never take any special considerations when inserting or deleting
> rows, and my dates are dates in my model, so...

for inserts & deletes , you need to track them , and it is a problem if the
data is from a backend database.

for sorting.

MM9492
MM9461
MM80753
MM5596

the sorting seems to "place" compare and ignores the length. that's what i
mean by 'broken'.  if you are sorting lists of part numbers, then it is not
the intended result.

steve
Roedy Green - 04 Oct 2005 09:14 GMT
>MM9492
>MM9461
[quoted text clipped - 4 lines]
>mean by 'broken'.  if you are sorting lists of part numbers, then it is not
>the intended result.

The sort just follows whatever natural order  Comparable you have
defined, right?

How would you like those items to sort? Perhaps I could show you how
to write a Comparable

Signature

Canadian Mind Products, Roedy Green.
http://mindprod.com Again taking new Java programming contracts.

steve - 04 Oct 2005 22:34 GMT
>> MM9492
>> MM9461
[quoted text clipped - 10 lines]
> How would you like those items to sort? Perhaps I could show you how
> to write a Comparable

yes  if you are volunteering ;-)

the reason my dates are broken, is more complex.
As all the data comes from the database, the dates are actually time-stamps,
which have the "microsecond part", to prevent longwinded conversions, ( i
read objects from the database not types, so that if i change my table
structure , my code does not break) for every date in every row , i just
converted to string, since for updates there is no class conversion for the
SQL for dates!!.
when i get time i will clean it up.

well normally i would prefer the letters to sort as letters & the "numbers"
as numbers"

MM123
MM9492
MM5596
MM80753
MM1234556

but we do not always use a 2 letter prefix, and some of our part numbers
(rarely) might be

12ASASA8545-878

i suspect the best way is to compare length then alpha, that should fix it.

but if possible i would like it to tie into the sun "broken" sorter.

this is how i am currently sorting.

   String[] colheads2 =
   { TableColMaintainance.HIDDENCOL, TableColMaintainance.HIDDENCOL1,
"Models" ,"Flags","Born"}; // hide index cols,&define visible cols
   private QueryTableModel modelqtbl = new QueryTableModel(colheads2,
query.NULLSUBQUERY); // pass in the col names needed, and an empty subquery
to get the table to display in the IDE
     TableSorter sorter = new TableSorter(modelqtbl);
   private JTable modelsList = new JTable(sorter);  //tie in the sorter.

and the sorter is this:

package utils;

import java.awt.*;
import java.awt.event.*;

import java.util.*;
import java.util.List;

import javax.swing.*;
import javax.swing.event.TableModelEvent;
import javax.swing.event.TableModelListener;
import javax.swing.table.*;

/**
* TableSorter is a decorator for TableModels; adding sorting
* functionality to a supplied TableModel. TableSorter does
* not store or copy the data in its TableModel; instead it maintains
* a map from the row indexes of the view to the row indexes of the
* model. As requests are made of the sorter (like getValueAt(row, col))
* they are passed to the underlying model after the row numbers
* have been translated via the internal mapping array. This way,
* the TableSorter appears to hold another copy of the table
* with the rows in a different order.
* <p/>
* TableSorter registers itself as a listener to the underlying model,
* just as the JTable itself would. Events recieved from the model
* are examined, sometimes manipulated (typically widened), and then
* passed on to the TableSorter's listeners (typically the JTable).
* If a change to the model has invalidated the order of TableSorter's
* rows, a note of this is made and the sorter will resort the
* rows the next time a value is requested.
* <p/>
* When the tableHeader property is set, either by using the
* setTableHeader() method or the two argument constructor, the
* table header may be used as a complete UI for TableSorter.
* The default renderer of the tableHeader is decorated with a renderer
* that indicates the sorting status of each column. In addition,
* a mouse listener is installed with the following behavior:
* <ul>
* <li>
* Mouse-click: Clears the sorting status of all other columns
* and advances the sorting status of that column through three
* values: {NOT_SORTED, ASCENDING, DESCENDING} (then back to
* NOT_SORTED again).
* <li>
* SHIFT-mouse-click: Clears the sorting status of all other columns
* and cycles the sorting status of the column through the same
* three values, in the opposite order: {NOT_SORTED, DESCENDING, ASCENDING}.
* <li>
* CONTROL-mouse-click and CONTROL-SHIFT-mouse-click: as above except
* that the changes to the column do not cancel the statuses of columns
* that are already sorting - giving a way to initiate a compound
* sort.
* </ul>
* <p/>
* This is a long overdue rewrite of a class of the same name that
* first appeared in the swing table demos in 1997.
*
* @author Philip Milne
* @author Brendon McLean
* @author Dan van Enckevort
* @author Parwinder Sekhon
* @version 2.0 02/27/04
*/
public class TableSorter extends AbstractTableModel {
   public static final int DESCENDING = -1;
   public static final int NOT_SORTED = 0;
   public static final int ASCENDING = 1;
   private static Directive EMPTY_DIRECTIVE = new Directive(-1,
NOT_SORTED);
   
   public static final Comparator COMPARABLE_COMAPRATOR = new Comparator()
{
           public int compare(Object o1, Object o2) {
               return ((Comparable) o1).compareTo(o2);
           }
       };

   public static final Comparator LEXICAL_COMPARATOR = new Comparator() {
           public int compare(Object o1, Object o2) {
               return o1.toString().compareTo(o2.toString());
           }
       };
private boolean TableEmpty=false;
   protected TableModel tableModel;
   private Row[] viewToModel;
   private int[] modelToView;
   private JTableHeader tableHeader;
   private MouseListener mouseListener;
   private TableModelListener tableModelListener;
   private Map columnComparators = new HashMap();
   private List sortingColumns = new ArrayList();

   public TableSorter() {
       this.mouseListener = new MouseHandler();
       this.tableModelListener = new TableModelHandler();
   }

   public TableSorter(TableModel tableModel) {
       this();
       setTableModel(tableModel);
   }

   public TableSorter(TableModel tableModel, JTableHeader tableHeader) {
       this();
       setTableHeader(tableHeader);
       setTableModel(tableModel);
   }

   private void clearSortingState() {
       viewToModel = null;
       modelToView = null;
   }

   public TableModel getTableModel() {
       return tableModel;
   }

   public void setTableModel(TableModel tableModel) {
       if (this.tableModel != null) {
           this.tableModel.removeTableModelListener(tableModelListener);
       }

       this.tableModel = tableModel;

       if (this.tableModel != null) {
           this.tableModel.addTableModelListener(tableModelListener);
       }

       clearSortingState();
       fireTableStructureChanged();
   }

   public JTableHeader getTableHeader() {
       return tableHeader;
   }

   public void setTableHeader(JTableHeader tableHeader) {
       if (this.tableHeader != null) {
           this.tableHeader.removeMouseListener(mouseListener);

           TableCellRenderer defaultRenderer =
this.tableHeader.getDefaultRenderer();

           if (defaultRenderer instanceof SortableHeaderRenderer) {
               this.tableHeader.setDefaultRenderer(
                   ((SortableHeaderRenderer)
defaultRenderer).tableCellRenderer);
           }
       }

       this.tableHeader = tableHeader;

       if (this.tableHeader != null) {
           this.tableHeader.addMouseListener(mouseListener);
           this.tableHeader.setDefaultRenderer(
               new SortableHeaderRenderer(
                   this.tableHeader.getDefaultRenderer()));
       }
   }

   public boolean isSorting() {
       return sortingColumns.size() != 0;
   }

   private Directive getDirective(int column) {
       for (int i = 0; i < sortingColumns.size(); i++) {
           Directive directive = (Directive) sortingColumns.get(i);

           if (directive.column == column) {
               return directive;
           }
       }

       return EMPTY_DIRECTIVE;
   }

   public int getSortingStatus(int column) {
       return getDirective(column).direction;
   }

   private void sortingStatusChanged() {
       clearSortingState();
       fireTableDataChanged();

       if (tableHeader != null) {
           tableHeader.repaint();
       }
   }

   public void setSortingStatus(int column, int status) {
       Directive directive = getDirective(column);

       if (directive != EMPTY_DIRECTIVE) {
           sortingColumns.remove(directive);
       }

       if (status != NOT_SORTED) {
           sortingColumns.add(new Directive(column, status));
       }

       sortingStatusChanged();
   }

   protected Icon getHeaderRendererIcon(int column, int ArrowSize) {
       Directive directive = getDirective(column);

       if (directive == EMPTY_DIRECTIVE) {
           return null;
       }

       return new Arrow(
           directive.direction == DESCENDING, ArrowSize,
           sortingColumns.indexOf(directive));
   }

   private void cancelSorting() {
       sortingColumns.clear();
       sortingStatusChanged();
   }

   public void setColumnComparator(Class type, Comparator comparator) {
       if (comparator == null) {
           columnComparators.remove(type);
       } else {
           columnComparators.put(type, comparator);
       }
   }

   protected Comparator getComparator(int column) {
       Class columnType = tableModel.getColumnClass(column);
       Comparator comparator = (Comparator)
columnComparators.get(columnType);

       if (comparator != null) {
           return comparator;
       }

       if (Comparable.class.isAssignableFrom(columnType)) {
           return COMPARABLE_COMAPRATOR;
       }

       return LEXICAL_COMPARATOR;
   }

   private Row[] getViewToModel() {
       if (viewToModel == null) {
           int tableModelRowCount = tableModel.getRowCount();
           viewToModel = new Row[tableModelRowCount];

           for (int row = 0; row < tableModelRowCount; row++) {
               viewToModel[row] = new Row(row);
           }
//if(tableModelRowCount==0){TableEmpty=true;}else{TableEmpty=false;}
           if (isSorting()) {
               Arrays.sort(viewToModel);
           }
       }

       return viewToModel;
   }

   public int modelIndex(int viewIndex) {
Row[] temprow=   getViewToModel(); // because we may need to initialize
it!!
   //if(TableEmpty==true){return 0;}else{
   //    return temprow [viewIndex].modelIndex;
  // }
   return getViewToModel()[viewIndex].modelIndex;
   }

   private int[] getModelToView() {
       if (modelToView == null) {
           int n = getViewToModel().length;
           modelToView = new int[n];

           for (int i = 0; i < n; i++) {
               modelToView[modelIndex(i)] = i;
           }
       }

       return modelToView;
   }

   // TableModel interface methods
   public int getRowCount() {
       return (tableModel == null) ? 0 : tableModel.getRowCount();
   }

   public int getColumnCount() {
       return (tableModel == null) ? 0 : tableModel.getColumnCount();
   }

   public String getColumnName(int column) {
       return tableModel.getColumnName(column);
   }

   public Class getColumnClass(int column) {
       return tableModel.getColumnClass(column);
   }

   public boolean isCellEditable(int row, int column) {
       return tableModel.isCellEditable(modelIndex(row), column);
   }

   public Object getValueAt(int row, int column) {
       return tableModel.getValueAt(modelIndex(row), column);
   }

   public void setValueAt(Object aValue, int row, int column) {
       tableModel.setValueAt(aValue, modelIndex(row), column);
   }

   // Helper classes
   private class Row implements Comparable {
       private int modelIndex;

       public Row(int index) {
           this.modelIndex = index;
       }

       public int compareTo(Object o) {
           int row1 = modelIndex;
           int row2 = ((Row) o).modelIndex;

           for (Iterator it = sortingColumns.iterator(); it.hasNext();) {
               Directive directive = (Directive) it.next();
               int column = directive.column;
               Object o1 = tableModel.getValueAt(row1, column);
               Object o2 = tableModel.getValueAt(row2, column);
//added by me
// Treat empty strings like nulls
       if (o1 instanceof String && (((String) o1).length() == 0)) {
           o1 = null;
       }

       if (o2 instanceof String && (((String) o2).length() == 0)) {
           o2 = null;
       }
       //end of added by me
               int comparison = 0;

               // Define null less than everything, except null.
               if ((o1 == null) && (o2 == null)) {
                   comparison = 0;
               } else if (o1 == null) {
                   comparison = -1;
               } else if (o2 == null) {
                   comparison = 1;
               } else
               //if (o1 instanceof Comparable && o2 instanceof Comparable)
               {
                   comparison = getComparator(column).compare(o1, o2);
               }
//else{comparison =o1.toString().compareTo(o2.toString());
//}
               if (comparison != 0) {
                   return (directive.direction == DESCENDING) ?
(-comparison)
                                                              : comparison;
               }
           }

           return 0;
       }
   }
/*
 // Sort nulls so they appear last, regardless
       // of sort order
       if ((o1 == null) && (o2 == null)) {
           return 0;
       } else if (o1 == null) {
           return 1;
       } else if (o2 == null) {
           return -1;
       } else if (o1 instanceof Comparable) {
           if (ascending) {
               return ((Comparable) o1).compareTo(o2);
           } else {
               return ((Comparable) o2).compareTo(o1);
           }
       } else {
           if (ascending) {
               return o1.toString().compareTo(o2.toString());
           } else {
               return o2.toString().compareTo(o1.toString());
           }
       }
*/
   private class TableModelHandler implements TableModelListener {
       public void tableChanged(TableModelEvent e) {
           // If we're not sorting by anything, just pass the event along.  
         
           if (!isSorting()) {
               clearSortingState();
               fireTableChanged(e);

               return;
           }

           //if the event is null , then eat it!!
           //there seems to be some sort of bug, making the event null!!
           if (e == null) {
               fireTableChanged(e);

               return;
           }

           // If the table structure has changed, cancel the sorting; the    
       
           // sorting columns may have been either moved or deleted from    
       
           // the model.
           if (e.getFirstRow() == TableModelEvent.HEADER_ROW) {
               cancelSorting();
               fireTableChanged(e);

               return;
           }

           // We can map a cell event through to the view without widening  
         
           // when the following conditions apply:
           //
           // a) all the changes are on one row (e.getFirstRow() ==
e.getLastRow()) and,
           // b) all the changes are in one column (column !=
TableModelEvent.ALL_COLUMNS) and,
           // c) we are not sorting on that column (getSortingStatus(column)
== NOT_SORTED) and,
           // d) a reverse lookup will not trigger a sort (modelToView !=
null)
           //
           // Note: INSERT and DELETE events fail this test as they have
column == ALL_COLUMNS.
           //
           // The last check, for (modelToView != null) is to see if
modelToView
           // is already allocated. If we don't do this check; sorting can
become
           // a performance bottleneck for applications where cells  
           // change rapidly in different parts of the table. If cells
           // change alternately in the sorting column and then outside of  
         
           // it this class can end up re-sorting on alternate cell updates
-
           // which can be a performance problem for large tables. The last
           // clause avoids this problem.
           int column = e.getColumn();

           if (
               (modelToView != null) && (e.getFirstRow() == e.getLastRow())
&&
                   (column != TableModelEvent.ALL_COLUMNS) &&
                   (getSortingStatus(column) == NOT_SORTED)) {
               int viewIndex = getModelToView()[e.getFirstRow()];
               fireTableChanged(
                   new TableModelEvent(
                       TableSorter.this, viewIndex, viewIndex, column,
                       e.getType()));

               return;
           }

           // Something has happened to the data that may have invalidated
the row order.
           clearSortingState();
           fireTableDataChanged();

           return;
       }
   }

   private class MouseHandler extends MouseAdapter {
       public void mouseClicked(MouseEvent e) {
           JTableHeader h = (JTableHeader) e.getSource();
           TableColumnModel columnModel = h.getColumnModel();
           int viewColumn = columnModel.getColumnIndexAtX(e.getX());
           int column = columnModel.getColumn(viewColumn).getModelIndex();

           if (column != -1) {
               int status = getSortingStatus(column);

               if (!e.isControlDown() && !e.isMetaDown()) {
                   cancelSorting();
               }

               // Cycle the sorting states through {NOT_SORTED, ASCENDING,
DESCENDING} or
               // {NOT_SORTED, DESCENDING, ASCENDING} depending on whether
shift is pressed.
               status = status + (e.isShiftDown() ? (-1) : 1);
               status = ((status + 4) % 3) - 1; // signed mod, returning
{-1, 0, 1}
               setSortingStatus(column, status);
           }
       }
   }

 private static class Arrow implements Icon {
       private boolean descending;
       private int size;
       private int priority;

       public Arrow(boolean descending, int size, int priority) {
           this.descending = descending;
           this.size = size;
           this.priority = priority;
       }

       public void paintIcon(Component c, Graphics g, int x, int y) {
           Color color = (c == null) ? Color.BLUE : c.getBackground();

//color = Color.RED;
           // In a compound sort, make each succesive triangle 20%
           // smaller than the previous one.
           //this is not such a good idea if there is a lot of cols to
sort!!
           
           int dx = (int) (size / 2 * Math.pow(0.8, priority));
           int dy = descending ? dx : (-dx);

           // Align icon (roughly) with font baseline.
           y = y + ((5 * size) / 6) + (descending ? (-dy) : 0);

           int shift = descending ? 1 : (-1);
           g.translate(x, y);

           // Right diagonal.
           g.setColor(color.darker());
           g.drawLine(dx / 2, dy, 0, 0);
           g.drawLine(dx / 2, dy + shift, 0, shift);

           // Left diagonal.
           g.setColor(color.brighter());
           g.drawLine(dx / 2, dy, dx, 0);
           g.drawLine(dx / 2, dy + shift, dx, shift);

           // Horizontal line.
           if (descending) {
               g.setColor(color.darker().darker());
           } else {
               g.setColor(color.brighter().brighter());
           }

           g.drawLine(dx, 0, 0, 0);

           g.setColor(color);
           g.translate(-x, -y);
       }

       public int getIconWidth() {
           return size;
       }

       public int getIconHeight() {
           return size;
       }
   }

   private class SortableHeaderRenderer implements TableCellRenderer {
       private TableCellRenderer tableCellRenderer;

       public SortableHeaderRenderer(TableCellRenderer tableCellRenderer) {
           this.tableCellRenderer = tableCellRenderer;
       }

       public Component getTableCellRendererComponent(
           JTable table, Object value, boolean isSelected, boolean hasFocus,
           int row, int column) {
           Component c = tableCellRenderer.getTableCellRendererComponent(
                   table, value, isSelected, hasFocus, row, column);

           if (c instanceof JLabel) {
               JLabel l = (JLabel) c;
               l.setHorizontalTextPosition(JLabel.LEFT);

               int modelColumn = table.convertColumnIndexToModel(column);
               //this sets the size of the arrow
               l.setIcon(
                   getHeaderRendererIcon(modelColumn,
l.getFont().getSize()+4));
           }

           return c;
       }
   }

   private static class Directive {
       private int column;
       private int direction;

       public Directive(int column, int direction) {
           this.column = column;
           this.direction = direction;
       }
   }
}
Roedy Green - 05 Oct 2005 02:49 GMT
>MM123
>MM9492
[quoted text clipped - 8 lines]
>
>i suspect the best way is to compare length then alpha, that should fix it.

Here is the code. I have only tested it with the test harness. Before
you trust it, it should be given examples to exercise all the code.

The big thing wrong with it is on every key compare it goes through a
great song and dance to parse the key into fragments.  Ideally that
should  be done once, not once per compare.

You could have Compare cache the values or your could precompute them
on a separate pass.

However, for a sort of only a few thousand you won't notice and big
slowdown.

It is a big ugly the kludge it uses to flush the accumulations for the
last key fragment.  I'd like to see a tidier way to handle that.  I
wanted to avoid repeating code just to handle the last frag.

// Comparator for Catalog Numbers. Sorts alpha and numeric parts
separately.
// Sorts numeric parts numerically.

import java.lang.StringBuilder;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Comparator;

class CatalogOrder implements Comparator<String>
  {
  /**
    * Compare two Strings. Callback for sort, as a catalog number.
    * effectively returns b-a;
    * @return +1, b>a, 0 a=b, -1 b<a, case insenstive
    */
  public final int compare( String a, String b )
     {
     // for speed could do tidyKey ahead of time, instead of calling
once per compare.

     Object[] aa = tidyKey( a );

     Object[] bb = tidyKey( b );

     int min = Math.min (aa.length, bb.length );
     long diff;

     for ( int i=0; i<min; i++ )
        {
        if ( aa[i] instanceof String && bb[i] instanceof String )
           {
           diff = ((String)aa[i]).compareTo( (String)bb[i] );
           }
        else if ( aa[i] instanceof Long && bb[i] instanceof Long )
           {
           diff = ((Long)aa[i]).longValue() -
((Long)bb[i]).longValue();
           }
        else
           {
           // Mismatched. Not a very consistent catalog number
system.
           // Treat numbers as less than letters.
           return aa[i] instanceof Long ? -1 : 1;
           }
        if ( diff != 0 )
           {
           // can't just return raw diff, since it is a long
           return diff < 0 ? -1 : 1;
           }
        } // end for
     // we fell out the bottom of the loop with detecting a
difference.
     // the longer key then comes later.
     if ( aa.length < bb.length ) return -1;
     if ( aa.length > bb.length ) return 1;
     // were absolutely identical
     return 0;

     } // end compare

  /**
   * Used create an aux sortable multifield key from a catalog
number.
   * Converts an alphanumeric key to an array of mixed Strings and
Longs
   * breaking catalog number into alpha and numeric fields
  */
  public static Object[] tidyKey ( String catalog )
     {

     // chars in this catalog number
     int catalogLength = catalog.length();

     // 0-terminated array to hold the catalog String
     // 0 lets us go round the loop one extra time to flush
accumulation.
     char[] catalogChars = new char[ catalogLength + 1];

     // copy catalog to char[]. Terminal 0 already in place.
     for ( int i=0; i<catalogLength; i++ )
        {
        catalogChars[i] = catalog.charAt( i );
        }

     // Where we accumulate the fragments of the the key, either
String or Long
     ArrayList<Object> keys = new ArrayList<Object>( 5 );

     // keep track if we are processing numbers or letters.
     boolean inNumeric = false;

     // where we accumulate the next chunk of key
     StringBuilder accum = new StringBuilder( catalogLength );

     // loop once for each char, plus one more time to flush the
append buffer.
     for ( int i=0; i<catalogLength+1; i++ )
        {
        char c = catalogChars[i];
        boolean isNumeric = '0'<= c && c <= '9';
        if ( isNumeric != inNumeric || c == 0 )
           {
           // we need to finish off the previous accumulation
           if ( inNumeric )
              {
              // finish off previous numeric field
              String numeric = accum.toString();
              if ( numeric.length() > 0 )
                 {
                 keys.add ( new Long( numeric ) );
                 }
              inNumeric = false;
              }
           else
              {
              // finish off previous alpha field
              String alpha = accum.toString();
              if ( alpha.length() > 0 )
                 {
                 keys.add ( alpha );
                 }
              inNumeric = true;
              }
           // clear for reuse
           accum.setLength( 0 );
           }

        // tack onto key fragment we have accumulated already,
        // or to newly cleared accum
        accum.append ( c );
        } // end for

     return keys.toArray( new Object[ keys.size() ] );
     } // end tidyKey

  /**
   * test harness
   *
   * @param args not used
   */
  public static void main ( String[] args )
     {

     // test tidyKey
     Object[] key = tidyKey( "12ASASA8545-878" );
     for ( Object keyFrag : key )
        {
        System.out.println ( keyFrag.getClass().getName() + " : " +
keyFrag.toString() );
        }

     // try sorting some catalog numbers

     String[] cats = {
        "MM123",
        "MM9492",
        "MM80753",
        "MM5596",
        "MM1234556",
        "12ASASA8545-878A",
        "12ASASA8545-878",
        "12ASASA8545-876",
     };
     Arrays.sort( cats, new CatalogOrder() );
     for ( String cat : cats )
        {
        System.out.println( cat );
        }
     } // end main
  }

Signature

Canadian Mind Products, Roedy Green.
http://mindprod.com Again taking new Java programming contracts.

steve - 06 Oct 2005 22:15 GMT
>> MM123
>> MM9492
[quoted text clipped - 198 lines]
>       } // end main
>    }

thanks roedy, I'm going to take a look at this over the weekend, whilst I
tidy up my 'date' implementation in the JTable.
Roedy Green - 03 Oct 2005 18:40 GMT
>The trick is that you DO just add the elements into your original table
>model, whereever you want to. The Sorter listens for new rows being
>added, and depending on how many are being added, either works out where
>to insert the single new row, or else reperforms the sort.

But why have two copies of the data?  Why not just work on the master
copy as I did?
Signature

Canadian Mind Products, Roedy Green.
http://mindprod.com Again taking new Java programming contracts.

Roedy Green - 03 Oct 2005 18:43 GMT
>>The trick is that you DO just add the elements into your original table
>>model, whereever you want to. The Sorter listens for new rows being
[quoted text clipped - 3 lines]
>But why have two copies of the data?  Why not just work on the master
>copy as I did?

Perhaps the thinking was that it was expensive the move the actual
data around, rather than  an index to the data. That is COBOL
thinking.  Moving the rows around just means moving a reference to the
row. You don't move the data for the row. Perhaps in some pathological
model it is expensive to move the rows.

Signature

Canadian Mind Products, Roedy Green.
http://mindprod.com Again taking new Java programming contracts.

jonck - 05 Oct 2005 00:50 GMT
> But why have two copies of the data?  Why not just work on the master
> copy as I did?

For me the advantage of this technique did not really become clear
until instead of a Sorter, I needed a Filter (not an "official" class,
but one I brewed up myself, it basically works like the Sorter except
it stores a subset of all the indices of the master model). In other
words, I wanted only certain rows of my table to be displayed based on
certain criteria entered by the user.
If I would have done this by actually deleting rows from my master
table, records would have been lost and every time the user wanted to
delete his previous query and enter a new one, I would have had to go
and retrieve the original data from the database/XML file/whatever.
Another advantage of this technique is that I could now layer Filters
and Sorters on top of each other, therefore giving the user the
capability of doing searches within resultsets of previous searches,
sorting in between, etc... (so one could, for example, do a search
like: give me everyone that lives in Paris, now everyone who's name
starts with a B, now sort on zipcode) and if a user wanted to
back-track a step, it was a simple case of deleting the last model in
the chain.

This type of functionality really showed me the advantage of delegating
models.

Kind regards, Jonck