Friday, January 26, 2007

TableViewers and Nativelooking Checkboxes

Many of us have faced the same problem that if you needed to use checkboxes in your TableViewer/TreeViewer they look not native because they are pictures. I had some free minutes and thought that it's time to create a LabelProvider which is able to create platform look-and-feel images out of the box. It's not 100% native but it's not far away. The trick is to automatically create screenshots from CheckBox-Buttons and use them. This way the checkboxes look native on all platforms and correspond to the current look and feel of the platform.

Here's the code if someone is interested:


package at.bestsolution.jface.viewers;

import org.eclipse.jface.resource.JFaceResources;
import org.eclipse.jface.viewers.ColumnLabelProvider;
import org.eclipse.jface.viewers.ColumnViewer;
import org.eclipse.swt.SWT;
import org.eclipse.swt.graphics.GC;
import org.eclipse.swt.graphics.Image;
import org.eclipse.swt.graphics.Point;
import org.eclipse.swt.widgets.Button;
import org.eclipse.swt.widgets.Shell;

public abstract class EmulatedNativeCheckBoxLabelProvider
extends ColumnLabelProvider
{
private static final String CHECKED_KEY = "CHECKED";
private static final String UNCHECK_KEY = "UNCHECKED";

public EmulatedNativeCheckBoxLabelProvider(ColumnViewer viewer) {
if( JFaceResources.getImageRegistry().getDescriptor(CHECKED_KEY) == null ) {
JFaceResources.getImageRegistry().put(UNCHECK_KEY,
makeShot(viewer.getControl().getShell(),false));
JFaceResources.getImageRegistry().put(CHECKED_KEY,
makeShot(viewer.getControl().getShell(),true));
}
}

private Image makeShot(Shell shell, boolean type) {
Shell s = new Shell(shell,SWT.NO_TRIM);
Button b = new Button(s,SWT.CHECK);
b.setSelection(type);
Point bsize = b.computeSize(SWT.DEFAULT, SWT.DEFAULT);
b.setSize(bsize);
b.setLocation(0, 0);
s.setSize(bsize);
s.open();

GC gc = new GC(b);
Image image = new Image(shell.getDisplay(), bsize.x, bsize.y);
gc.copyArea(image, 0, 0);
gc.dispose();

s.close();

return image;
}

public Image getImage(Object element) {
if( isChecked(element) ) {
return JFaceResources.getImageRegistry().
getDescriptor(CHECKED_KEY).createImage();
} else {
return JFaceResources.getImageRegistry().
getDescriptor(UNCHECK_KEY).createImage();
}
}

protected abstract boolean isChecked(Object element);
}

I haven't really tested this (currently only on WinXP) but I suppose it's working on all platforms. You can also get the code from my svn-repository which holds some other interesting utilities and viewer classes.

Thursday, January 18, 2007

The new faces of JFace (part2)

The last time I showed you the new programming model we introduced to JFace to make it feel like programming SWT.

But has this been the only reason that we decided to add this new API beside the existing one (ILabelProvider, ITableLabelProvider, IColorProvider, ITableColorProvider, IFontProvider, ITableFontProvider) .
Sure enough it was not the only reason. First of all many newcomers have been confused by all those NON MANDATORY interfaces to control different aspects of items and we faced the problem that whenever SWT-Provided a new feature e.g. Owner Draw Support in 3.2 we would have to add a new interface type and to support new features e.g. ToolTip support for table/tree cells we also had to provide a new interface. We decided that this is not the way to go for the future.

So we sat down and thought about a complete new structure below JFace tree and table support. We added the idea of rows (ViewerRow) and cells (ViewerCell) abstracting common things provided of TreeItem and TableItem and completely hiding the widget specific things into this class. The abstraction level this provided to us made it possible to provide a common class named ColumnViewer.

So what have we learned so far:
  1. We don't need all those interfaces any more
  2. We have a new abstraction level for column based viewers (ViewerRow, ViewerCell)
  3. We have a new base class named ColumnViewer
So you may ask what you should use if you can't use the old interfaces any more. Well the answer is ColumnLabelProvider or if you want to implement the whole cell behaviour your own it's abstract base class named CellLabelProvider. The new ColumnLabelProvider combines all currently know interfaces (ILabelProvider, IFontProvider, IColorProvider). There's no base class visible to you supporting ITable*-interfaces because those interface put limitations to tables I'll show you later let's now explore how to use the new ColumnLabelProvider interface and how it is used.

TableViewer viewer = new TableViewer(parent,SWT.FULL_SELECTION);

TableViewerColumn column = new TableViewerColumn(viewer,SWT.NONE);
column.setLabelProvider(new StockNameProvider());
column.getColumn().setText("Name");

TableViewerColumn column = new TableViewerColumn(viewer,SWT.NONE);
column.setLabelProvider(new StockValueProvider());
column.getColumn().setText("Modification");

public class StockNameProvider extends ColumnLableProvider {
@Override
public String getText(Object element) {
return ((Stock)element).name;
}
}

public class StockValueProvider extends ColumnLabelProvider {

@Override
public String getText(Object element) {
return ((Stock)element).modification.doubleValue() + "%";
}

@Override
public Color getForeground(Object element) {
if( ((Stock)element)modiciation.doubleValue() < 0 ) {
return getDisplay().getSystemColor(SWT.COLOR_RED);
} else {
return getDisplay().getSystemColor(SWT.COLOR_GREEN);
}
}
}

You may now argue that this was easier with the old ITable*-API and you are true but from the point of reusability this version is much more flexible and you can reuse your LabelProviders for many different TableViewers because they don't hold any index informations. Another thing is that you needed a bunch of custom code if you wanted the columns to be reordable with the old API this is not an issue any more with the new API because the LabelProvider is directly connected to the column and moves with it.

Next time I'll continue with some nice new LabelProvider features like ToolTip support and OwnerDraw.

Friday, January 05, 2007

What items are visible in Table/Tree?

Have you often asked yourself which items are visibile in Table/Tree. Maybe you need these informations to dispose system-resources (e.g. if you have different images in every row you can easily run out of system resources).

What to do? There's no SWT-API available to calculate this information. So we need a bunch of custom code to make this work. The following codefragements are only a first rough test case and they need JFace from 3.3M4 and a patch from bug 151295 .

Step 1: Setup the infrastructure
- Event object to inform consumers about the change of visible rows
public class ViewerRowStateChangedEvent extends EventObject {
private static final long serialVersionUID = 1L;

public ArrayList itemsHidden;

public ArrayList itemsVisible;

public ViewerRowStateChangedEvent(Object source) {
super(source);
}
}
- ViewerRowStateChangeListener for consumers to implement
public interface ViewerRowStateChangeListener {
public void itemStateChangedListener(ViewerRowStateChangedEvent event);
}
Step 2: Create an AbstractClass implementing the visible row logic on base of ViewerRow and the new API whichn will hopefully added by bug 151295.
public abstract class AbstractViewerRowVisibilityStateSupport {
private ArrayList currentItems = new ArrayList();

private ColumnViewer columnViewer;

private ListenerList listenerList = new ListenerList();

public AbstractViewerRowVisibilityStateSupport(ColumnViewer columnViewer) {
this.columnViewer = columnViewer;

Listener l = new Listener() {
public void handleEvent(Event event) {
ArrayList list = recalculateVisibleItems();
ArrayList itemsVisible = new ArrayList();

Iterator it = list.iterator();
Object obj;

while( it.hasNext() ) {
obj = it.next();

if( ! currentItems.remove(obj) ) {
itemsVisible.add(obj);
}
}

ArrayList hiddenItems = currentItems;
currentItems = list;

if( itemsVisible.size() > 0 || hiddenItems.size() > 0 ) {
if( ! listenerList.isEmpty() ) {
ColumnViewer v;
v = AbstractViewerRowVisibilityStateSupport.this.columnViewer;

ViewerRowStateChangedEvent ev = new ViewerRowStateChangedEvent(v);
ev.itemsHidden = hiddenItems;
ev.itemsVisible = itemsVisible;

Object[] listeners = listenerList.getListeners();
ViewerRowStateChangeListener l;

for( int i = 0; i < listeners.length; i++ ) {
l = (ViewerRowStateChangeListener)listeners[i];
l.itemStateChangedListener(ev);
}
}
}
}
};
addListeners(getControl(),l);
}

protected abstract void addListeners(Scrollable control, Listener l);

protected abstract ViewerRow getTopRow();

protected Scrollable getControl() {
return (Scrollable)columnViewer.getControl();
}

public void addItemStateListener(ViewerRowStateChangeListener listener) {
listenerList.add(listener);
}

private ArrayList recalculateVisibleItems() {
ArrayList list = new ArrayList(100);
ViewerRow topRow = getTopRow();

if( topRow != null ) {
int totalHeight = getControl().getClientArea().height;
int itemHeight = topRow.getBounds().height;

list.add(topRow);

int tmp = topRow.getBounds().x+itemHeight;
// tmp += itemHeight;
// this would be more precise but half rows
// would be marked as non-visible

// run until we reached the end of the client-area
while( tmp < totalHeight ) {
tmp += itemHeight;
topRow = topRow.getNeighbor(ViewerRow.BELOW, false);

if( topRow == null ) {
break;
}

list.add(topRow);
}
}

return list;
}
}
We simply listen to events who can change the items shown in the Scrollable make a diff to the last state and inform all consumers about the change.

But what are the events how modify the items shown? This is delegated to specialized classes for Table and Tree because those may differ from control to control.

Step 3: Provide specialized implementation for Tree and Table
- An implementation for SWT-Table
public class TableViewerRowVisibilityStateSupport extends
AbstractViewerRowVisibilityStateSupport {
public TableViewerRowVisibilityStateSupport(TableViewer columnViewer) {
super(columnViewer);
}

protected void addListeners(Scrollable control, Listener l) {
control.getVerticalBar().addListener(SWT.Selection, l);
control.addListener(SWT.Resize, l);
control.addListener(SWT.KeyUp, l);
}

protected ViewerRow getTopRow() {
Table t = (Table)getControl();
int index = t.getTopIndex();
TableItem topItem = t.getItem(index);

if( topItem != null ) {
return (ViewerRow) topItem.getData(ViewerRow.ROWPART_KEY);
}

return null;
}
}
- An implementation for SWT-Tree
public class TreeViewerRowVisibilityStateSupport extends
AbstractViewerRowVisibilityStateSupport {
public TreeViewerRowVisibilityStateSupport(TreeViewer columnViewer) {
super(columnViewer);
}

protected void addListeners(Scrollable control, Listener l) {
control.getVerticalBar().addListener(SWT.Selection, l);
control.addListener(SWT.Resize, l);
control.addListener(SWT.MouseUp, l);
control.addListener(SWT.KeyUp, l);
}

protected ViewerRow getTopRow() {
TreeItem topItem = ((Tree)getControl()).getTopItem();
if( topItem != null ) {
return (ViewerRow) topItem.getData(ViewerRow.ROWPART_KEY);
}
return null;
}
}
As said this is very rough first draft how this could work. I'll post this got some coments about possible issues this could provoke.