2

I'm using a JTable with say 3 columns, displaying information about files and folders:

Col1: Either "file" or "folder" (String)
Col2: Name of the file or folder (String)
Col3: Creation date (Timstamp)

I can easily sort the table by any column. But I'm not able to achieve the following desired behavior:

No matter what column header the user clicks, folders should always display before files. So basically sorting by Col1 in any order other than descending should not be possible. Also, if sorted by Col3, any folders should still display before files, regardless of their timestamp.

Any hint to how to do this is greatly appreciated.

Yours

Andreas

alex2410
  • 10,904
  • 3
  • 25
  • 41
Andreas
  • 31
  • 1
  • 2
  • you have two issues here: a) keep a particular value always on top of another, independent on sort order - that's not possible without changes deep down the rowSorter's bowels. b) sort individual columns with knowledge about others: that can be done by a custom model which has a row object, a compound comparator comparing that row object and a custom sorter which sees that row object for each column (have a look at sun.awt.shell.FilePane for an example) – kleopatra Nov 08 '13 at 10:50

3 Answers3

0

Try to use next code it helps you:

public class Example extends JFrame {

public Example() {
    DefaultTableModel defaultTableModel = new DefaultTableModel(new Object[][]{{"1"},{"3"},{"2"}},new Object[]{"text"});
    JTable t = new JTable(defaultTableModel);
    TableRowSorter<TableModel> sorter = new TableRowSorter<TableModel>(defaultTableModel);
    sorter.setComparator(0, new Comparator<Object>() {
            @Override
            public int compare(Object arg0, Object arg1) {
                return arg0.toString().compareTo(arg1.toString());
            }
    });

    t.setRowSorter(sorter);
    add(new JScrollPane(t));
    setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
    pack();
    setVisible(true);
}

public static void main(String...strings ){
    Example e = new Example();
}

}

Here I set custom Comparator to TableRowSorter for column 0. You can compare your rows in different ways with your own Comparator implementation. Try it!

Comparator

RowSorter tutorial

alex2410
  • 10,904
  • 3
  • 25
  • 41
  • Thanks for the suggestion but I believe this will just sort column 1 by String. After a click on column 3 it would sort by Timestamp; files and folders in column 1 could be mixed. The table should still be primarily ordered by column 1 and only secondarily by column 3. Same way for a click on column 2 (still sorted by 1 and than by 2). – Andreas Nov 07 '13 at 21:37
  • I shown you how to set custom `Comparator` to `RowSorter`, write your logic inside it and you will get your result. In `compare` method get rows for objects and sort it at first by file or folder, after that if it second column by second order and next.... Write your own comparator for that. – alex2410 Nov 07 '13 at 21:44
0

Maybe this is a way to do it. There's probably a more elegant way to do it around.

The DefaultRowSorter Comparator only compares values for one column at a time, without additional information it alone can not make sure that one column in the set of columns (say column 1) must always be the first column to sort by.

If you mess with DefaultRowSorter.toggleSortOrder and add a new SortKey to keys at position 0 before setSortKeys gets called (this would be the primary column you always want to sort by first [column 1]), the ascending and descending arrows displayed in the column headers constantly mark column 1 (rightfully so), but this is not what the user expects if she clicks on column 2.

Another approach is to have Comparator know about column sort relationships. An easy way is to add information about sort precedences to the Objects for Comparator.compare. Simple precedences can be represented by a prefix String (for example: prefix "a" for column 1 and prefix "b" for all other columns).

PrefixedData can contain String, Boolean, Timestamp or null values and a prefix:

public class PrefixedData { 
private String sData=null, prefix="";
private Timestamp tData=null;
private Boolean bData=null;


public void setData(String data) {
    sData=data;     
}

public void setData(Timestamp data) {
    tData=data;     
}

public void setData(boolean data) {
    bData=data;     
}

public void setPrefix(String prefix) {
    this.prefix=prefix;
}

public String getPrefix() {
    return prefix;
}           

public Object getData() {       
    if(sData!=null) return sData;
    if(tData!=null) return tData;
    if(bData!=null) return bData;           
    return null;
}
}

JTable cells can be rendered like this for String, Boolean and Timestamp, if your table model returns PrefixedData for DefaultTableModel.getValueAt:

private static class PrefixedDataRenderer3 extends JLabel implements TableCellRenderer {

    private static final SimpleDateFormat formatter = new SimpleDateFormat("MM.yyyy  HH:mm:ss");
    private static final JCheckBox box = new JCheckBox();

    public PrefixedDataRenderer() {

        setOpaque(true);
    }

    public Component getTableCellRendererComponent(JTable table, Object value, boolean isSelected, boolean hasFocus, int row, int column) {

        remove(box);
        PrefixedData p=(PrefixedData) value;

        if(p.getData()==null) return this;

        if(p.getData().getClass().equals(String.class)) 
            {
            setText((String)p.getData());
                 return this;
            }

        if(p.getData().getClass().equals(Timestamp.class))
        {
            setText(formatter.format((Timestamp)p.getData()));
            return this;
        }

        if(p.getData().getClass().equals(Boolean.class)) 
            {
            box.setSelected(((Boolean)p.getData()).booleanValue());
            add(box);
            }   

        return this;
    }
}

And finally the Comparator can decide the order of things:

Comparator<Object> PrefixedDataComparator = new Comparator<Object>() {

    private List<? extends SortKey> sortKeys;

    @Override
    public int compare(Object o1, Object o2) {

        PrefixedData p1=(PrefixedData) o1;
        PrefixedData p2=(PrefixedData) o2;

        //First: compare prefixes (precedence data)         
        int prefixResult=p1.getPrefix().compareTo(p2.getPrefix());

        sortKeys = tableOU.getRowSorter().getSortKeys();

        //The prefixes are not the same, so return the result of the prefix comparision
        //The result has to be inverted if  we're sorting in descending order
        //for the column the user has clicked

        if(prefixResult!=0) return (sortKeys.get(0).getSortOrder().equals(SortOrder.ASCENDING) ? prefixResult : -prefixResult );

        //Only if the prefixes are the same do we have to compare the payload data          


        //Try to impose an order for null           
        if(p1.getData()==null && p2.getData()!=null) return -1;
        if(p1.getData()==null && p2.getData()==null) return 0;              
        if(p1.getData()!=null && p2.getData()==null) return 1;

        //Objects compared are identical            
        if(p1.getData().equals(p2.getData())) return 0;


        //Compare String
        if(p1.getData().getClass().equals(String.class)) {
            String s1=(String) p1.getData();
            String s2=(String) p2.getData();
            return s1.toLowerCase().compareTo(s2.toLowerCase());
        }

        //Compare Timestamp
        if(p1.getData().getClass().equals(Timestamp.class)) {
            Timestamp t1=(Timestamp) p1.getData();
            Timestamp t2=(Timestamp) p2.getData();
            if(t1.before(t2)) return -1;
            if(t1.equals(t2)) return 0;             
            return 1;
        }           

        //Compare Bool
        if(p1.getData().getClass().equals(Boolean.class)) {         
            boolean b1=((Boolean)p1.getData()).booleanValue();
            boolean b2=((Boolean)p2.getData()).booleanValue();
            if(b1==false && b2==true) return -1;
            if(b1==b2) return 0;                
            return 1;
        }


        return 0;
    }

};

Any suggestions to this or different approaches a very welcome.

Andreas
  • 31
  • 1
  • 2
0

Well, the question is old but Google returned this page to me when I had a similar task. So, I'll post here my solution. Hope that it would help someone.

import java.lang.reflect.Field;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.HashSet;
import java.util.List;
import java.util.Set;
import javax.swing.DefaultRowSorter;
import javax.swing.JTable;
import javax.swing.RowSorter.SortKey;
import javax.swing.SortOrder;
import javax.swing.table.TableModel;
import javax.swing.table.TableRowSorter;
import org.apache.log4j.Logger;

public class PredefinedRowSorter extends TableRowSorter< TableModel > {
  private static final Logger LOGGER = Logger.getLogger( PredefinedRowSorter.class );
  private static final SortKey EMPTY_ARRAY[] = new SortKey[ 0 ];

  private final JTable table;
  private SortKey preColumns[] = EMPTY_ARRAY;
  private SortKey postColumns[] = EMPTY_ARRAY;

  public PredefinedRowSorter( JTable table ) {
    super( table.getModel() );
    this.table = table;
  }

  private void check( SortKey modelColumns[], SortKey crossCheckColumns[], boolean post ) {
    TableModel tm = table.getModel();
    int max = tm.getColumnCount();
    Set< Integer > used = new HashSet< Integer >();
    for ( SortKey key : modelColumns ) {
      if ( key == null )
        throw new IllegalArgumentException( "SortKey must be non-null" );
      if ( key.getColumn() < 0 )
        throw new IllegalArgumentException( "SortKey column must be non-negative" );
      if ( key.getColumn() >= max )
        throw new IllegalArgumentException( "SortKey column is too high (out of model scope)" );
      if ( key.getSortOrder() == SortOrder.UNSORTED )
        throw new IllegalArgumentException( "SortKey must be ordered (ascending or descending)" );
      if ( !used.add( key.getColumn() ) )
        throw new IllegalArgumentException( "SortKey column must be unique (column " + key.getColumn() + " is repeating)" );
    }
    for ( SortKey key : crossCheckColumns )
      if ( used.contains( key.getColumn() ) )
        throw new IllegalArgumentException( "SortKey column must be unique (column " + key.getColumn() + " is already contained in " + ( post ? "post" : "pre" ) + " columns list)" );
  }

  public PredefinedRowSorter withPreColumns( SortKey... modelColumns ) {
    if ( modelColumns == null )
      modelColumns = EMPTY_ARRAY;
    if ( !Arrays.equals( preColumns, modelColumns ) ) {
      check( modelColumns, postColumns, true );
      preColumns = modelColumns;
      setSortKeys( getSortKeys() );
    }
    return this;
  }

  public PredefinedRowSorter withPostColumns( SortKey... modelColumns ) {
    if ( modelColumns == null )
      modelColumns = EMPTY_ARRAY;
    if ( !Arrays.equals( postColumns, modelColumns ) ) {
      check( modelColumns, preColumns, false );
      postColumns = modelColumns;
      setSortKeys( getSortKeys() );
    }
    return this;
  }

  public JTable getTable() {
    return table;
  }

  public SortKey[] getPreColumns() {
    return preColumns.length == 0 ? preColumns : preColumns.clone();
  }

  public SortKey[] getPostColumns() {
    return postColumns.length == 0 ? postColumns : postColumns.clone();
  }

  private void setSortKeysInternal( List< ? extends SortKey > sortKeys ) {
    try {
      Field field = DefaultRowSorter.class.getDeclaredField( "sortKeys" );
      boolean accessible = field.isAccessible();
      if ( !accessible )
        field.setAccessible( true );
      field.set( this, sortKeys );
      if ( !accessible )
        field.setAccessible( false );
    } catch ( IllegalAccessException e ) {
      LOGGER.error( null, e );
    } catch ( IllegalArgumentException e ) {
      LOGGER.error( null, e );
    } catch ( NoSuchFieldException e ) {
      LOGGER.error( null, e );
    } catch ( SecurityException e ) {
      LOGGER.error( null, e );
    }
  }

  private Object getViewToModelInternal() {
    try {
      Field field = DefaultRowSorter.class.getDeclaredField( "viewToModel" );
      boolean accessible = field.isAccessible();
      if ( !accessible )
        field.setAccessible( true );
      Object ret = field.get( this );
      if ( !accessible )
        field.setAccessible( false );
      return ret;
    } catch ( IllegalAccessException e ) {
      LOGGER.error( null, e );
    } catch ( IllegalArgumentException e ) {
      LOGGER.error( null, e );
    } catch ( NoSuchFieldException e ) {
      LOGGER.error( null, e );
    } catch ( SecurityException e ) {
      LOGGER.error( null, e );
    }
    return null;
  }

  private void sortExistingDataInternal() {
    try {
      Method method = DefaultRowSorter.class.getDeclaredMethod( "sortExistingData" );
      boolean accessible = method.isAccessible();
      if ( !accessible )
        method.setAccessible( true );
      method.invoke( this );
      if ( !accessible )
        method.setAccessible( false );
    } catch ( IllegalAccessException e ) {
      LOGGER.error( null, e );
    } catch ( IllegalArgumentException e ) {
      LOGGER.error( null, e );
    } catch ( NoSuchMethodException e ) {
      LOGGER.error( null, e );
    } catch ( InvocationTargetException e ) {
      LOGGER.error( null, e );
      LOGGER.error( null, ( ( InvocationTargetException )e ).getCause() );
    }
  }

  @Override
  public void setSortKeys( List< ? extends SortKey > sortKeys ) {
    List< ? extends SortKey > oldSortKeys = getSortKeys();
    List< ? extends SortKey > newSortKeys;
    if ( sortKeys != null && !sortKeys.isEmpty() ) {
      int max = getModelWrapper().getColumnCount();
      for ( SortKey key : sortKeys )
        if ( key == null || key.getColumn() < 0 || key.getColumn() >= max )
          throw new IllegalArgumentException( "Invalid SortKey" );
      newSortKeys = Collections.unmodifiableList( new ArrayList< SortKey >( sortKeys ) );
    } else
      newSortKeys = Collections.emptyList();
    setSortKeysInternal( newSortKeys );
    if ( !newSortKeys.equals( oldSortKeys ) ) {
      fireSortOrderChanged();
      boolean wasChanged = false;
      if ( preColumns.length > 0 || postColumns.length > 0 ) {
        List< SortKey > editableSortKeys = new ArrayList< SortKey >( newSortKeys );
        for ( int i = preColumns.length - 1; i >= 0; i-- ) {
          int modelColumn = preColumns[ i ].getColumn();
          int idx = indexOfColumn( editableSortKeys, preColumns.length - i - 1, editableSortKeys.size(), modelColumn );
          SortOrder sortOrder = idx < 0 ? preColumns[ i ].getSortOrder() : editableSortKeys.remove( idx ).getSortOrder();
          editableSortKeys.add( 0, new SortKey( modelColumn, sortOrder ) );
        }
        int to = editableSortKeys.size();
        for ( SortKey postColumn : postColumns ) {
          int modelColumn = postColumn.getColumn();
          int idx = indexOfColumn( editableSortKeys, preColumns.length, to, modelColumn );
          SortOrder sortOrder;
          if ( idx < 0 )
            sortOrder = postColumn.getSortOrder();
          else {
            sortOrder = editableSortKeys.remove( idx ).getSortOrder();
            to--;
          }
          editableSortKeys.add( new SortKey( modelColumn, sortOrder ) );
        }
        if ( wasChanged = !editableSortKeys.equals( newSortKeys ) )
          setSortKeysInternal( editableSortKeys );
      }
      if ( getViewToModelInternal() == null )
        sort();
      else
        sortExistingDataInternal();
      if ( wasChanged )
        setSortKeysInternal( newSortKeys );
    }
  }

  private int indexOfColumn( List< SortKey > sortKeys, int fromIncl, int toExcl, int column ) {
    for ( int i = toExcl - 1; i >= fromIncl; i-- )
      if ( sortKeys.get( i ).getColumn() == column )
        return i;
    return -1;
  }
};

Usage:

table.setRowSorter( new PredefinedRowSorter( table )
  .withPreColumns( new SortKey( 0, SortOrder.ASCENDING ),
                   new SortKey( 1, SortOrder.ASCENDING ) ) );

This would set a preceding sorting on first two columns.

Post-sorting is available.

An end-user may sort these two columns too, thus switching the order of sorting (ascending/descending).

Also, this class is available as a part of JBroTable library (source code).

The principle of action is so: predefined columns are added to a sorting columns list before sorting and removed from this list after sorting. Adding and removal is performed via reflection because DefaultRowSorter implementation doesn't provide an access to the list.

Qualtagh
  • 721
  • 6
  • 15