NatTable with custom scrollbars

4 minute read

When talking about styling a SWT control via CSS, one issue is raised quite early. The scrollbars can not be styled! Looking at a dark theme, the importance on that issue becomes obvious, as you can see in the following screenshot.

Using NatTable the scrolling capabilities are via the ViewportLayer. With NatTable 1.1 the possibility was added to set custom scrollbars to the ViewportLayer. This enables for example to have multiple ViewportLayer in a layer composition (split viewport) or to create UI layouts with special scrolling interactions.

With the possibility to use a custom scrollbar implementation, it is possible to style a NatTable completely with a dark theme. As an example for a stylable scrollbar we use the FlatScrollBar from Code Affine.

Since the scrollbars of the Canvas, which is the base class of NatTable, can’t be exchanged directly, we need to create a wrapper composite for the NatTable. This way the scrollbars can be attached beneath the NatTable instead of being part inside the NatTable.

To create the above layout, a GridLayout with two columns can be used, where the NatTable will take all the available space.

// NatTable and scrollbar container 
Composite container = new Composite(parent, SWT.NONE); 
GridLayoutFactory
    .swtDefaults()
    .numColumns(2)
    .margins(0, 0)
    .spacing(0, 0)
    .applyTo(container);

// NatTable as main control 
NatTable natTable = new NatTable(container, viewportLayer); 
GridDataFactory
    .fillDefaults()
    .grab(true, true)
    .applyTo(natTable); 

The vertical scrollbar is attached to the right, and the horizontal scrollbar is attached to the bottom. To ensure that the layout doesn’t break, the FlatScrollBar is wrapped into a Composite. This way we are also able to set a fixed width/height, while telling the FlatScrollBar to fill the available space.

// vertical scrollbar wrapped in another composite for layout 
Composite verticalComposite = new Composite(container, SWT.NONE); 
GridLayoutFactory
    .swtDefaults()
    .margins(0, 0)
    .spacing(0, 0)
    .applyTo(verticalComposite); 
GridData verticalData = GridDataFactory
    .swtDefaults()
    .hint(14, SWT.DEFAULT)
    .align(SWT.BEGINNING, SWT.FILL)
    .grab(false, true)
    .create(); 
verticalComposite.setLayoutData(verticalData);

FlatScrollBar vertical = new FlatScrollBar(verticalComposite, SWT.VERTICAL); 
GridDataFactory
    .fillDefaults()
    .grab(true, true)
    .applyTo(vertical);

// horizontal scrollbar wrapped in another composite for layout 
Composite horizontalComposite = new Composite(container, SWT.NONE); 
GridLayoutFactory
    .swtDefaults()
    .margins(0, 0)
    .spacing(0, 0)
    .applyTo(horizontalComposite); 
GridData horizontalData = GridDataFactory
    .swtDefaults()
    .hint(SWT.DEFAULT, 14)
    .align(SWT.FILL, SWT.BEGINNING)
    .grab(true, false)
    .create();
horizontalComposite.setLayoutData(horizontalData);

FlatScrollBar horizontal = new FlatScrollBar(horizontalComposite, SWT.HORIZONTAL); 
GridDataFactory
    .fillDefaults()
    .grab(true, true)
    .applyTo(horizontal); 

To be independent of the scrollbar implementation, the IScroller<T> interface was introduced in NatTable. The two default implementations ScrollBarScroller and SliderScroller are shipped with NatTable Core to be able to set custom scrollbars using SWT default implementations. Using this abstraction it is also possible to use another scrollbar implementation, like the FlatScrollBar. The following code shows the implementation of a FlatScrollBarScroller.

class FlatScrollBarScroller implements IScroller<FlatScrollBar> {

	private FlatScrollBar scrollBar;
	public FlatScrollBarScroller(FlatScrollBar scrollBar) { 
		this.scrollBar = scrollBar; 
	}

	@Override 
	public FlatScrollBar getUnderlying() { 
		return scrollBar; 
	}

	@Override 
	public boolean isDisposed() { 
		return scrollBar.isDisposed(); 
	}

	@Override 
	public void addListener(int eventType, Listener listener) { 
		scrollBar.addListener(eventType, listener); 
	}

	@Override 
	public void removeListener(int eventType, Listener listener) { 
		scrollBar.removeListener(eventType, listener); 
	}

	@Override 
	public int getSelection() { 
		return scrollBar.getSelection(); 
	}

	@Override 
	public void setSelection(int value) { 
		scrollBar.setSelection(value); 
	}

	@Override 
	public int getMaximum() { 
		return scrollBar.getMaximum(); 
	}

	@Override 
	public void setMaximum(int value) { 
		scrollBar.setMaximum(value); 
	}

	@Override 
	public int getPageIncrement() { 
		return scrollBar.getPageIncrement(); 
	}

	@Override 
	public void setPageIncrement(int value) { 
		scrollBar.setPageIncrement(value); 
	}

	@Override 
	public int getThumb() { 
		return scrollBar.getThumb(); 
	}

	@Override 
	public void setThumb(int value) { 
		scrollBar.setThumb(value); 
	}

	@Override 
	public int getIncrement() { 
		return scrollBar.getIncrement(); 
	}

	@Override 
	public void setIncrement(int value) { 
		scrollBar.setIncrement(value); 
	}

	@Override 
	public boolean getEnabled() { 
		return scrollBar.getEnabled(); 
	}

	@Override 
	public void setEnabled(boolean b) { 
		scrollBar.setEnabled(b); 
	}

	@Override 
	public boolean getVisible() { 
		return scrollBar.getVisible(); 
	}

	@Override 
	public void setVisible(boolean b) { 
		scrollBar.setVisible(b); 
	}
}

Using the above FlatScrollBarScroller, the created FlatScrollBar instances can be set to the ViewportLayer.

As the layout will always show the space for the scroller with the GridData instances above, we need to register a listener that hides the wrapper Composites of the FlatScrollBar instances in case the FlatScrollBar is hidden, and a listener that shows the Composites again in case the FlatScrollBar becomes visible again. This is done by setting a GridLayoutData with a matching exclude flag.

// create the vertical scroller 
FlatScrollBarScroller verticalScroller = new FlatScrollBarScroller(vertical);

// register the hide/show listener 
verticalScroller.addListener(SWT.Hide, new Listener() { 
	@Override 
	public void handleEvent(Event event) { 
		GridDataFactory
                    .createFrom(verticalData)
                    .exclude(true)
		    .applyTo(verticalComposite); 
		GridDataFactory
                    .createFrom(horizontalData)
		    .span(2, 1)
                    .applyTo(horizontalComposite); 
	} 
}); 
verticalScroller.addListener(SWT.Show, new Listener() { 
	@Override 
	public void handleEvent(Event event) { 
		verticalComposite.setLayoutData(verticalData); 
		horizontalComposite.setLayoutData(horizontalData); 
	} 
});

// create the horizontal scroller 
FlatScrollBarScroller horizontalScroller = new FlatScrollBarScroller(horizontal);

// register the hide/show listener 
horizontalScroller.addListener(SWT.Hide, new Listener() { 
	@Override 
	public void handleEvent(Event event) { 
		GridDataFactory
                    .createFrom(verticalData)
                    .span(1, 2)
                    .applyTo(verticalComposite); 
		GridDataFactory
                    .createFrom(horizontalData)
                    .exclude(true)
                    .applyTo(horizontalComposite); 
	} 
}); 
horizontalScroller.addListener(SWT.Show, new Listener() { 
	@Override 
	public void handleEvent(Event event) { 
		verticalComposite.setLayoutData(verticalData); 
		horizontalComposite.setLayoutData(horizontalData); 
	} 
});

// set the custom IScroller to the ViewportLayer 
viewportLayer.setVerticalScroller(verticalScroller); 
viewportLayer.setHorizontalScroller(horizontalScroller); 

The last part is to set the style information to the NatTable and the FlatScrollBar instances.

// set a dark background to the wrapper container 
container.setBackground(GUIHelper.COLOR_BLACK);

// set a dark styling to the scrollbars 
vertical.setBackground(GUIHelper.COLOR_BLACK); 
vertical.setPageIncrementColor(GUIHelper.COLOR_BLACK); 
vertical.setThumbColor(GUIHelper.COLOR_DARK_GRAY);

horizontal.setBackground(GUIHelper.COLOR_BLACK); 
horizontal.setPageIncrementColor(GUIHelper.COLOR_BLACK); 
horizontal.setThumbColor(GUIHelper.COLOR_DARK_GRAY);

// set a dark styling to NatTable 
natTable.setBackground(GUIHelper.COLOR_BLACK); 
natTable.setTheme(new DarkNatTableThemeConfiguration()); 

Doing the steps described above it is possible to create a completely dark themed NatTable using custom scrollbars as shown in the picture below.

At the time writing this blog post, there is no wrapper or adapter implementation in NatTable for creating a NatTable with custom scrollbars. But it might be added in the future, based on the above explanations.

The full example code is available here.

Updated: