NatTable with custom scrollbars
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.