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.