Tuesday, July 24, 2007

JFace TableViewer Sorting using LabelProvider

While designing some code for an Eclipse plugin, I decided that I wanted to be able to provide the normal sort mechanism for the tables that is present in so many other applications. I was actually surprised to find that there wasn't a simple API to just turn this on. I sure there are good reasons for this and there are many bug reports in this area. It even looks like there are some fixes in progress.
Anyway, while looking at some examples, I came across this snippet by Tom Schindl. It was really close to what I was looking for, but didn't quite work because it assumed a simple model and didn't exercise the LabelProvider to get the text to sort.

Since I usually create the columns in a loop, it was easy to modify the compare method to look up the index of the column using a label provider.

int i = 0;
for (String name : colNames) {
final int colIdx = i;
TableColumn column = new TableColumn(viewer.getTable(), SWT.NONE);
column.setWidth(200);
column.setText(name);
column.setMoveable(true);

TableColumnSorter cSorter = new TableColumnSorter(viewer, column) {
protected int doCompare(Viewer v, Object e1, Object e2) {
ITableLabelProvider lp = ((ITableLabelProvider) viewer
.getLabelProvider());
String t1 = lp.getColumnText(e1, colIdx);
String t2 = lp.getColumnText(e2, colIdx);
return t1.compareTo(t2);
}
};

cSorter.setSorter(cSorter, TableColumnSorter.ASC);
i++;
}




The full text of the example with my changes are below the split.

package snippet;

/*******************************************************************************
* TableViewerSorting Example
*
* Adam Cabler
*
* revised example by Tom Schindl
* http://dev.eclipse.org/viewcvs/index.cgi/org.eclipse.jface.snippets/Eclipse%20JFace%20Snippets/org/eclipse/jface/snippets/viewers/Snippet040TableViewerSorting.java?view=markup
*
* removed ColumnViewer references
* added ITableLabelProvider
* used label provider for text compare
*
* All rights reserved. This program and the accompanying materials
* are made available under the terms of the Eclipse Public License v1.0
* which accompanies this distribution, and is available at
* http://www.eclipse.org/legal/epl-v10.html
*
*
*******************************************************************************/


import org.eclipse.jface.viewers.ILabelProviderListener;
import org.eclipse.jface.viewers.IStructuredContentProvider;
import org.eclipse.jface.viewers.ITableLabelProvider;
import org.eclipse.jface.viewers.TableViewer;
import org.eclipse.jface.viewers.Viewer;
import org.eclipse.jface.viewers.ViewerComparator;
import org.eclipse.swt.SWT;
import org.eclipse.swt.events.SelectionAdapter;
import org.eclipse.swt.events.SelectionEvent;
import org.eclipse.swt.graphics.Image;
import org.eclipse.swt.layout.FillLayout;
import org.eclipse.swt.widgets.Display;
import org.eclipse.swt.widgets.Shell;
import org.eclipse.swt.widgets.TableColumn;

/**
* Table sorting example using label provider
*
* @author cabler
*
*/

public class TableViewerSortingExample {

private class MyContentProvider implements IStructuredContentProvider {

public Object[] getElements(Object inputElement) {
return (Person[]) inputElement;
}

public void dispose() {
}

public void inputChanged(Viewer viewer, Object oldInput, Object newInput) {
}
}

private class MyLabelProvider implements ITableLabelProvider {

@Override
public Image getColumnImage(Object element, int columnIndex) {
// TODO Auto-generated method stub
return null;
}

@Override
public String getColumnText(Object element, int columnIndex) {
if (!(element instanceof Person)) {
return null;
}
Person p = (Person) element;
switch (columnIndex) {
case 0:
return p.givenname;
case 1:
return p.surname;
case 2:
return p.email;
}
return "Error";
}

@Override
public void addListener(ILabelProviderListener listener) {
// TODO Auto-generated method stub

}

@Override
public void dispose() {
// TODO Auto-generated method stub

}

@Override
public boolean isLabelProperty(Object element, String property) {
// TODO Auto-generated method stub
return false;
}

@Override
public void removeListener(ILabelProviderListener listener) {
// TODO Auto-generated method stub

}

}

public class Person {
public String givenname;
public String surname;
public String email;

public Person(String givenname, String surname, String email) {
this.givenname = givenname;
this.surname = surname;
this.email = email;
}

}

public TableViewerSortingExample(Shell shell) {
final TableViewer viewer = new TableViewer(shell, SWT.BORDER | SWT.FULL_SELECTION);
viewer.setContentProvider(new MyContentProvider());
viewer.setLabelProvider(new MyLabelProvider());
String[] colNames = new String[] { "Givenname", "Surname", "Email" };

int i = 0;
for (String name : colNames) {
final int colIdx = i;
TableColumn column = new TableColumn(viewer.getTable(), SWT.NONE);
column.setWidth(200);
column.setText(name);
column.setMoveable(true);

TableColumnSorter cSorter = new TableColumnSorter(viewer, column) {
protected int doCompare(Viewer v, Object e1, Object e2) {
ITableLabelProvider lp = ((ITableLabelProvider) viewer
.getLabelProvider());
String t1 = lp.getColumnText(e1, colIdx);
String t2 = lp.getColumnText(e2, colIdx);
return t1.compareTo(t2);
}
};

cSorter.setSorter(cSorter, TableColumnSorter.ASC);
i++;
}

Person[] model = createModel();
viewer.setInput(model);
viewer.getTable().setLinesVisible(true);
viewer.getTable().setHeaderVisible(true);

}

private Person[] createModel() {
Person[] elements = new Person[4];
elements[0] = new Person("Tom", "Schindl",
"tom.schindl@bestsolution.at");
elements[1] = new Person("Boris", "Bokowski",
"Boris_Bokowski@ca.ibm.com");
elements[2] = new Person("Tod", "Creasey", "Tod_Creasey@ca.ibm.com");
elements[3] = new Person("Wayne", "Beaton", "wayne@eclipse.org");

return elements;
}

private static abstract class TableColumnSorter extends ViewerComparator {
public static final int ASC = 1;

public static final int NONE = 0;

public static final int DESC = -1;

private int direction = 0;

private TableColumn column;

private TableViewer viewer;

public TableColumnSorter(TableViewer viewer, TableColumn column) {
this.column = column;
this.viewer = viewer;
this.column.addSelectionListener(new SelectionAdapter() {

public void widgetSelected(SelectionEvent e) {
if (TableColumnSorter.this.viewer.getComparator() != null) {
if (TableColumnSorter.this.viewer.getComparator() == TableColumnSorter.this) {
int tdirection = TableColumnSorter.this.direction;

if (tdirection == ASC) {
setSorter(TableColumnSorter.this, DESC);
} else if (tdirection == DESC) {
setSorter(TableColumnSorter.this, NONE);
}
} else {
setSorter(TableColumnSorter.this, ASC);
}
} else {
setSorter(TableColumnSorter.this, ASC);
}
}
});
}

public void setSorter(TableColumnSorter sorter, int direction) {
if (direction == NONE) {
column.getParent().setSortColumn(null);
column.getParent().setSortDirection(SWT.NONE);
viewer.setComparator(null);
} else {
column.getParent().setSortColumn(column);
sorter.direction = direction;

if (direction == ASC) {
column.getParent().setSortDirection(SWT.DOWN);
} else {
column.getParent().setSortDirection(SWT.UP);
}

if (viewer.getComparator() == sorter) {
viewer.refresh();
} else {
viewer.setComparator(sorter);
}

}
}

public int compare(Viewer viewer, Object e1, Object e2) {
return direction * doCompare(viewer, e1, e2);
}

protected abstract int doCompare(Viewer TableViewer, Object e1, Object e2);
}

/**
* @param args
*/

public static void main(String[] args) {
Display display = new Display();

Shell shell = new Shell(display);
shell.setLayout(new FillLayout());
new TableViewerSortingExample(shell);
shell.open();

while (!shell.isDisposed()) {
if (!display.readAndDispatch())
display.sleep();
}

display.dispose();

}

}

6 comments:

Ilya Shinkarenko said...

well, what about sorting values of datatypes other than String? ;)

Adam Cabler said...

Sure, that would be additional work, but most of the time, users want the values to be sorted based on what they see, which is the text, rather than some other value intrinsic to the object. I think this behavior would be pretty unexpected to the user.

Tom said...

The only reason is that we didn't had time for an API in 3.3. We first needed to fix all those inconsistency we inherited and we did a fairly good job. Does this code also work if you are reordering the columns? The bug request is https://bugs.eclipse.org/bugs/show_bug.cgi?id=166153?

We are always open for contributions. Think about an API and we'll help you to evolve it and if we are all satisfied the feature can be added. Most likely you should start with ViewerColumn-API which will be the home for such a JFace feature.

Adam Cabler said...

Hi Tom,
Thanks for the feedback. This does work if reordered since it uses the original index. Also, a name based lookup could be used just as well.
And it would be really nice to get something like this in for 3.4. If nobody is working on this, I would be happy to suggest something.

Tom said...

but your label provider is not prepared to work with reordered indices? Or am I missing something?

Adam Cabler said...

That's true, the label provider doesn't do anything special for reordered indices, but in this case there's no need. The compare method always uses the initial index and so does the label provider, so they should always match. Since this is on a column basis, it shouldn't care what order it is in the table. (I could easily have something wrong here, so please let me know)
However, if there were any issues with this, a method could be added to the inner class to look for a column by its name. But the label provider would have to be changed as well.