=== modified file 'src/com/goldencode/p2j/persist/orm/UnclosablePreparedStatement.java'
--- src/com/goldencode/p2j/persist/orm/UnclosablePreparedStatement.java	2021-01-13 21:04:41 +0000
+++ src/com/goldencode/p2j/persist/orm/UnclosablePreparedStatement.java	2021-06-10 11:58:36 +0000
@@ -369,5 +369,11 @@
       this.target.setBlob(parameterIndex, inputStream); }
    public void setNClob(int parameterIndex, Reader reader) throws SQLException { 
       this.target.setNClob(parameterIndex, reader); }
+
+   @Override
+   public String toString()
+   {
+      return target.toString();
+   }
    
 }

=== modified file 'src/com/goldencode/p2j/ui/BrowseColumnWidget.java'
--- src/com/goldencode/p2j/ui/BrowseColumnWidget.java	2021-04-24 22:09:31 +0000
+++ src/com/goldencode/p2j/ui/BrowseColumnWidget.java	2021-06-10 11:58:36 +0000
@@ -2659,8 +2659,8 @@
    /**
     * Sets the BGCOLOR writable attribute.  Private internal version.
     *
-    * @param    fgColor
-    *           The new value for the FGCOLOR attribute.
+    * @param    bgColor
+    *           The new value for the BGCOLOR attribute.
     */
    private void setBgColorInt(NumberType bgColor)
    {

=== modified file 'src/com/goldencode/p2j/ui/TreeConfig.java'
--- src/com/goldencode/p2j/ui/TreeConfig.java	2021-06-01 19:14:11 +0000
+++ src/com/goldencode/p2j/ui/TreeConfig.java	2021-06-17 18:07:04 +0000
@@ -95,7 +95,7 @@
  * @param <N> 
  *        node type
  */
-public abstract class TreeConfig<N extends TreeNodeEntry>
+public abstract class TreeConfig
 extends WidgetConfigExt
 {
    /** Node id of the currently selected node */
@@ -123,7 +123,7 @@
    public boolean sorted;
 
    /** The flat list of tree nodes */
-   public ArrayList<N> nodes = new ArrayList<>();
+   public List<TreeNodeEntry> nodes = new ArrayList<>();
 
    /** Indentation pixel size */
    public int indentation;
@@ -140,8 +140,8 @@
    /** Flag to toggle expand on ENTER */
    public boolean expandOnEnter;
 
-   /** The current top node */
-   public int topNode;
+   /** The current top node ID or -1 if tree is empty */
+   public int topNode = -1;
 
    /** Flag to toggle expand node icons visibility */
    public boolean showExpandIcon = true;
@@ -205,9 +205,11 @@
 
    /**
     * Create node entry instance.
+    * 
     * @return node entry instance
     */
-   protected abstract Supplier<N> createNodeEntry();
+   protected abstract Supplier<TreeNodeEntry> createNodeEntry();
+   
    /**
     * Create a new config and associate it with the given widget.
     *
@@ -309,7 +311,7 @@
    {
       super.applyConfig(config);
 
-      TreeConfig<N> cfg = (TreeConfig<N>) config;
+      TreeConfig cfg = (TreeConfig) config;
       selectedNodeId = cfg.selectedNodeId;
       textEdit = cfg.textEdit;
       dragDrop = cfg.dragDrop;
@@ -369,7 +371,7 @@
       showExpandIcon       = (booleanAttrs & ATTR_MASK_SHOW_EXPAND_ICON) == ATTR_MASK_SHOW_EXPAND_ICON;
       removeNodeOnCollapse = (booleanAttrs & ATTR_MASK_REM_NODE_COLLAPSE) == ATTR_MASK_REM_NODE_COLLAPSE;
       
-      nodes = new ArrayList<N>((List<N>)readList(in, createNodeEntry()));
+      nodes = new ArrayList<TreeNodeEntry>(readList(in, createNodeEntry()));
       scrollNodeCount = in.readInt();
       draggedNode = in.readInt();
       draggedOverNode = in.readInt();

=== modified file 'src/com/goldencode/p2j/ui/TreeListConfig.java' (properties changed: +x to -x)
--- src/com/goldencode/p2j/ui/TreeListConfig.java	2021-06-07 16:01:45 +0000
+++ src/com/goldencode/p2j/ui/TreeListConfig.java	2021-06-17 18:05:43 +0000
@@ -93,8 +93,8 @@
  * TreeList widget configuration.
  * @param <N> node entry type
  */
-public class TreeListConfig<N extends TreeListNodeEntry>
-extends TreeConfig<N>
+public class TreeListConfig
+extends TreeConfig
 {
    /** List of column configurations, index 0 is the column showing the tree portion of the widget */
    public List<Column> columns = new ArrayList<>();
@@ -189,7 +189,7 @@
    {
       super.applyConfig(config);
 
-      TreeListConfig<N> cfg = (TreeListConfig<N>) config;
+      TreeListConfig cfg = (TreeListConfig) config;
       columns = cfg.columns;
       fixedColumn = cfg.fixedColumn;
       showHeader = cfg.showHeader;
@@ -212,7 +212,7 @@
    {
       super.applyConfig(config);
 
-      final TreeListConfig<N> cfg = (TreeListConfig<N>) config;
+      final TreeListConfig cfg = (TreeListConfig) config;
       fixedColumn = cfg.fixedColumn;
       showHeader = cfg.showHeader;
       triggerColumn = cfg.triggerColumn;
@@ -309,9 +309,9 @@
     * @return node entry instance
     */
    @Override
-   protected Supplier<N> createNodeEntry()
+   protected Supplier<TreeNodeEntry> createNodeEntry()
    {
-      return () -> (N)new TreeListNodeEntry();
+      return () -> new TreeListNodeEntry();
    }
 
    /**

=== modified file 'src/com/goldencode/p2j/ui/TreeListNodeEntry.java'
--- src/com/goldencode/p2j/ui/TreeListNodeEntry.java	2021-06-01 15:53:18 +0000
+++ src/com/goldencode/p2j/ui/TreeListNodeEntry.java	2021-06-16 22:20:48 +0000
@@ -95,6 +95,7 @@
     */
    public TreeListNodeEntry()
    {
+      // no-op
    }
 
    /**
@@ -117,13 +118,10 @@
     * @param   cellIndex
     *          Cell index.
     *
-    * @return  cell value.
-    *
-    * @throws  IndexOutOfBoundsException
-    *          if the index is out of range
+    * @return  cell value or {@code null} if the index is out of range of valid indexes
     */
    @Override
-   public Object getValue(int cellIndex)
+   public Object getValue(final int cellIndex)
    {
       return cellIndex >= 0 && cellIndex < cellValues.size() ? cellValues.get(cellIndex) : null;
    }

=== modified file 'src/com/goldencode/p2j/ui/TreeListNodeResource.java'
--- src/com/goldencode/p2j/ui/TreeListNodeResource.java	2021-06-07 16:01:45 +0000
+++ src/com/goldencode/p2j/ui/TreeListNodeResource.java	2021-06-17 17:48:09 +0000
@@ -79,8 +79,8 @@
  * @param <N> 
  *        node type
  */
-public class TreeListNodeResource<N extends TreeListNodeEntry>
-extends TreeNodeResource<N>
+public class TreeListNodeResource
+extends TreeNodeResource
 implements TreeListNode
 {
    /**
@@ -114,10 +114,10 @@
     * @param   parent
     *          Node parent.
     */
-   public TreeListNodeResource(TreeListWidget<N> treeList,
-                               int nodeId,
-                               String key,
-                               TreeListNodeResource<N> parent)
+   public TreeListNodeResource(final TreeWidgetBase treeList,
+                               final int nodeId,
+                               final String key,
+                               final TreeNodeFace parent)
    {
       super(treeList, nodeId, key, parent);
       cellValues.add("");
@@ -137,11 +137,11 @@
     * @param   parent
     *          Node parent.
     */
-   public TreeListNodeResource(final TreeListWidget<N> treeList,
+   public TreeListNodeResource(final TreeWidgetBase treeList,
                                final int nodeId,
                                final String key,
                                final String text,
-                               final TreeListNodeResource<N> parent)
+                               final TreeNodeFace parent)
    {
       super(treeList, nodeId, key, parent);
       cellStrings.add(text);
@@ -155,7 +155,7 @@
    @Override
    public character getNodeText()
    {
-      return new character((String) cellStrings.get(0));
+      return new character(cellStrings.get(0));
    }
 
    /**
@@ -179,10 +179,10 @@
     * @return  new instance of {@link TreeListNodeEntry}.
     */
    @Override
-   protected N saveToEntry()
+   protected TreeNodeEntry saveToEntry()
    {
-      int parentId = parent != null ? parent.nodeId : Integer.MIN_VALUE;
-      N entry = (N)new TreeListNodeEntry(nodeId, parentId);
+      int parentId = parent != null ? ((TreeNodeResource)parent).nodeId : Integer.MIN_VALUE;
+      TreeListNodeEntry entry = new TreeListNodeEntry(nodeId, parentId);
       super.saveToEntry(entry);
       entry.cellValues.addAll(cellValues);
       entry.cellFGColors.addAll(cellFGColors);
@@ -203,7 +203,7 @@
     * @return the cell value
     */
    public final <T> T getCellValue(final int colIndex,
-                                   final Function<TreeListNodeResource<N>, List<T>> getValues)
+                                   final Function<TreeNodeFace, List<T>> getValues)
    {
       final List<T> values = getValues.apply(this);
       return (colIndex >= values.size()) ? null : values.get(colIndex);
@@ -215,7 +215,7 @@
       {
          case STRING:
          case ICON:
-            return getCellValue(colIndex, n -> n.cellStrings);
+            return getCellValue(colIndex, n -> ((TreeListNodeResource)n).cellStrings);
 
          default:
             TreeWidgetBase.LOG.severe("Node comparing: dataType not supported: " + dataType);

=== modified file 'src/com/goldencode/p2j/ui/TreeListWidget.java' (properties changed: +x to -x)
--- src/com/goldencode/p2j/ui/TreeListWidget.java	2021-06-10 11:50:04 +0000
+++ src/com/goldencode/p2j/ui/TreeListWidget.java	2021-06-17 18:05:42 +0000
@@ -109,8 +109,8 @@
  * @param <N> 
  *        node type
  */
-public class TreeListWidget<N extends TreeListNodeEntry>
-extends TreeWidgetBase<N, TreeListConfig<N>, TreeListNodeResource<N>>
+public class TreeListWidget
+extends TreeWidgetBase
 implements TreeList
 {
    /** Flag marking columns or column position changes are pending */
@@ -143,7 +143,7 @@
     */
    public TreeListWidget(boolean dynamic)
    {
-      this(dynamic, new TreeListConfig<N>());
+      this(dynamic, new TreeListConfig());
 
       clearAll();
 
@@ -160,7 +160,7 @@
     * @param   config
     *          The config to use to create widget.
     */
-   public TreeListWidget(boolean dynamic, TreeListConfig<N> config)
+   public TreeListWidget(boolean dynamic, TreeListConfig config)
    {
       super(dynamic, config);
    }
@@ -176,7 +176,7 @@
    @Override
    public integer getSortedColumns()
    {
-      final List<Column> ccolumns = getAttr("columns", () -> config.columns);
+      final List<Column> ccolumns = getAttr("columns", () -> ((TreeListConfig)config).columns);
       for (int i = ccolumns.size() - 1; i >= 0; i--)
       {
          if(ccolumns.get(i).sort != Sorting.NONE)
@@ -194,7 +194,7 @@
    @Override
    public integer getTriggerColumn()
    {
-      return new integer(getAttr("triggerColumn", () -> config.triggerColumn));
+      return new integer(getAttr("triggerColumn", () -> ((TreeListConfig)config).triggerColumn));
    }
 
    /**
@@ -210,7 +210,7 @@
    {
       // assert colIndex == null && !colIndex.isUnknown();
       final int sortedColumn = colIndex.intValue();
-      final List<Column> ccolumns = getAttr("columns", () -> config.columns);
+      final List<Column> ccolumns = getAttr("columns", () -> ((TreeListConfig)config).columns);
 
       for (int i = ccolumns.size() - 1; i >= 0; i--)
       {
@@ -236,7 +236,7 @@
    @Override
    public integer getSortedColumnCount()
    {
-      return new integer(getAttr("columns", () -> config.columns).stream()
+      return new integer(getAttr("columns", () -> ((TreeListConfig)config).columns).stream()
                .filter(c -> c.sort != Sorting.NONE).count());
    }
 
@@ -257,7 +257,7 @@
     *          Column index. Set negative value or unknown to unfix all columns.
     */
    @Override
-   public void fixColumnsLeft(NumberType colIndex)
+   public void fixColumnsLeft(final NumberType colIndex)
    {
       short newValue = -1;
       if (colIndex != null && !colIndex.isUnknown())
@@ -266,9 +266,9 @@
          newValue = (short) (index < 0 ? -1 : index);
       }
 
-      if (newValue != getAttr("fixedColumn", () -> config.fixedColumn))
+      if (newValue != getAttr("fixedColumn", () -> ((TreeListConfig)config).fixedColumn))
       {
-         pushWidgetAttr("fixedColumn", config.fixedColumn);
+         pushWidgetAttr("fixedColumn", ((TreeListConfig)config).fixedColumn);
       }
    }
 
@@ -302,8 +302,12 @@
     * @return  the column index.
     */
    @Override
-   public integer createColumn(character caption, NumberType dataType, NumberType width,
-            NumberType align, NumberType bgColor, NumberType fgColor)
+   public integer createColumn(final character caption,
+                               final NumberType dataType,
+                               final NumberType width,
+                               final NumberType align,
+                               final NumberType bgColor,
+                               final NumberType fgColor)
    {
       if (!assertKnown(caption, dataType))
       {
@@ -351,7 +355,7 @@
       {
          if (pendingPushColumns)
          {
-            setAttr("columns", config.columns, columns, v -> config.columns = new ArrayList(v));
+            setAttr("columns", ((TreeListConfig)config).columns, columns, v -> ((TreeListConfig)config).columns = new ArrayList(v));
             pendingPushColumns = false;
         }
       }
@@ -368,7 +372,7 @@
    @Override
    public void setColumnCaption(final NumberType index, final character caption)
    {
-      final TreeListConfig.Column col = config.getColumn(index);
+      final TreeListConfig.Column col = ((TreeListConfig)config).getColumn(index);
       if (col == null)
       {
          LOG.warning("Invalid column index: " + index);
@@ -398,7 +402,7 @@
    public integer getColumnWidth(NumberType index)
    {
       flushWidgetAttrs();
-      TreeListConfig.Column col = config.getColumn(index);
+      TreeListConfig.Column col = ((TreeListConfig)config).getColumn(index);
       if (col != null)
       {
          return new integer(col.width);
@@ -425,7 +429,7 @@
          return;
       }
 
-      final TreeListConfig.Column col = config.getColumn(index);
+      final TreeListConfig.Column col = ((TreeListConfig)config).getColumn(index);
       if (col == null)
       {
          LOG.warning("Invalid column index: " + index);
@@ -494,7 +498,7 @@
    @Override
    public logical isShowHeader()
    {
-      return new logical(getAttr("showHeader", () -> config.showHeader));
+      return new logical(getAttr("showHeader", () -> ((TreeListConfig)config).showHeader));
    }
 
    /**
@@ -512,7 +516,7 @@
          return;
       }
 
-      setAttr("showHeader", config.showHeader, value.booleanValue(), (vv) -> config.showHeader = vv);
+      setAttr("showHeader", ((TreeListConfig)config).showHeader, value.booleanValue(), (vv) -> ((TreeListConfig)config).showHeader = vv);
    }
 
    /**
@@ -523,7 +527,7 @@
    @Override
    public character getThousandSeparator()
    {
-      return new character(getAttr("thousandSeparator", () -> config.thousandSeparator));
+      return new character(getAttr("thousandSeparator", () -> ((TreeListConfig)config).thousandSeparator));
    }
 
    /**
@@ -540,7 +544,7 @@
          return;
       }
 
-      setAttr("thousandSeparator", config.thousandSeparator, value.toStringMessage(), (vv) -> config.thousandSeparator = vv);
+      setAttr("thousandSeparator", ((TreeListConfig)config).thousandSeparator, value.toStringMessage(), (vv) -> ((TreeListConfig)config).thousandSeparator = vv);
    }
 
    /**
@@ -551,7 +555,7 @@
    @Override
    public character getDecimalSeparator()
    {
-      return new character(getAttr("decimalSeparator", () -> config.decimalSeparator));
+      return new character(getAttr("decimalSeparator", () -> ((TreeListConfig)config).decimalSeparator));
    }
 
    /**
@@ -568,8 +572,8 @@
          return;
       }
 
-      setAttr("decimalSeparator", config.decimalSeparator, value.toStringMessage(),
-               (vv) -> config.decimalSeparator = vv);
+      setAttr("decimalSeparator", ((TreeListConfig)config).decimalSeparator, value.toStringMessage(),
+               (vv) -> ((TreeListConfig)config).decimalSeparator = vv);
    }
 
    /**
@@ -580,7 +584,7 @@
    @Override
    public character getDateSeparator()
    {
-      return new character(getAttr("dateSeparator", () -> config.dateSeparator));
+      return new character(getAttr("dateSeparator", () -> ((TreeListConfig)config).dateSeparator));
    }
 
    /**
@@ -597,7 +601,7 @@
          return;
       }
 
-      setAttr("dateSeparator", config.dateSeparator, value.toStringMessage(), (vv) -> config.dateSeparator = vv);
+      setAttr("dateSeparator", ((TreeListConfig)config).dateSeparator, value.toStringMessage(), (vv) -> ((TreeListConfig)config).dateSeparator = vv);
    }
 
    /**
@@ -608,7 +612,7 @@
    @Override
    public character getShortDateFormat()
    {
-      return new character(getAttr("shortDateFormat", () -> config.shortDateFormat));
+      return new character(getAttr("shortDateFormat", () -> ((TreeListConfig)config).shortDateFormat));
    }
 
    /**
@@ -625,8 +629,8 @@
          return;
       }
 
-      setAttr("shortDateFormat", config.shortDateFormat, value.toStringMessage(), 
-            (vv) -> config.shortDateFormat = vv);
+      setAttr("shortDateFormat", ((TreeListConfig)config).shortDateFormat, value.toStringMessage(), 
+            (vv) -> ((TreeListConfig)config).shortDateFormat = vv);
    }
 
    /**
@@ -641,7 +645,7 @@
    public logical isColumnVisible(NumberType index)
    {
       flushWidgetAttrs();
-      TreeListConfig.Column col = config.getColumn(index);
+      TreeListConfig.Column col = ((TreeListConfig)config).getColumn(index);
       if (col != null)
       {
          return new logical(col.visible);
@@ -665,7 +669,7 @@
          return;
       }
 
-      final TreeListConfig.Column col = config.getColumn(colIndex);
+      final TreeListConfig.Column col = ((TreeListConfig)config).getColumn(colIndex);
       if (col == null)
       {
          return;
@@ -695,7 +699,7 @@
    @Override
    public void setCellIcon(NumberType nodeId, NumberType colIndex, NumberType imgId)
    {
-      setCellValue(nodeId, colIndex, n -> n.cellValues, imgId);
+      setCellValue(nodeId, colIndex, n -> ((TreeListNodeResource)n).cellValues, imgId);
    }
 
    /**
@@ -736,7 +740,7 @@
          return;
       }
 
-      final Object cellValue = getCellValue(nodeId, colIndex, n -> n.cellValues);
+      final Object cellValue = getCellValue(nodeId, colIndex, n -> ((TreeListNodeResource)n).cellValues);
       
       if(cellValue == null)
       {
@@ -761,7 +765,7 @@
    @Override
    public void setCellString(final NumberType nodeId, final NumberType colIndex, final character value)
    {
-      setCellValue(nodeId, colIndex, n -> n.cellValues, value.toStringMessage());
+      setCellValue(nodeId, colIndex, n -> ((TreeListNodeResource)n).cellValues, value.toStringMessage());
    }
 
    /**
@@ -830,7 +834,7 @@
    @Override
    public integer getCellFgColor(NumberType nodeId, NumberType colIndex)
    {
-      final ColorRgb cellValue = getCellValue(nodeId, colIndex, n -> n.cellFGColors);
+      final ColorRgb cellValue = getCellValue(nodeId, colIndex, n -> ((TreeListNodeResource)n).cellFGColors);
       return cellValue != null ? new integer(cellValue.getGuiRgb()) : new integer();
    }
 
@@ -849,7 +853,8 @@
    @Override
    public integer getCellBgColor(NumberType nodeId, NumberType colIndex)
    {
-      final ColorRgb cellValue = getCellValue(nodeId, colIndex, n -> n.cellBGColors);
+      final ColorRgb cellValue = getCellValue(nodeId, colIndex,
+               n -> ((TreeListNodeResource) n).cellBGColors);
       return cellValue != null ? new integer(cellValue.getGuiRgb()) : new integer();
    }
 
@@ -880,7 +885,7 @@
     *          the owner's config was modified.
     */
    @Override
-   public void afterConfigUpdate(TreeListConfig beforeUpdate)
+   public void afterConfigUpdate(TreeConfig beforeUpdate)
    {
       super.afterConfigUpdate(beforeUpdate);
       
@@ -894,7 +899,7 @@
        * to compare columns, since no proper equals() method is defined for columns. 
        */
       columns.clear();
-      columns.addAll(config.columns);
+      columns.addAll(((TreeListConfig)config).columns);
    }
 
    /**
@@ -921,11 +926,11 @@
          return new integer();
       }
 
-      final List<Integer> cellImageList = config.cellImageList;
+      final List<Integer> cellImageList = ((TreeListConfig)config).cellImageList;
       final int imageIndex = cellImageList.size();
-      final ArrayList<Integer> newList = new ArrayList<>(cellImageList);
+      final List<Integer> newList = new ArrayList<>(cellImageList);
       newList.add(globalImageId);
-      setAttr("cellImageList", cellImageList, newList, dummy -> config.cellImageList = newList);
+      setAttr("cellImageList", cellImageList, newList, dummy -> ((TreeListConfig)config).cellImageList = newList);
 
       return new integer(imageIndex);
    }
@@ -943,7 +948,8 @@
    @Override
    public void setCellFgColor(final NumberType nodeId, final NumberType colIndex, final NumberType color)
    {
-      setCellValue(nodeId, colIndex, n -> n.cellFGColors, new ColorRgb(color.intValue()));
+      setCellValue(nodeId, colIndex, n -> ((TreeListNodeResource) n).cellFGColors,
+               new ColorRgb(color.intValue()));
    }
 
    /**
@@ -959,7 +965,8 @@
    @Override
    public void setCellBgColor(final NumberType nodeId, final NumberType colIndex, final NumberType color)
    {
-      setCellValue(nodeId, colIndex, n -> n.cellBGColors, new ColorRgb(color.intValue()));
+      setCellValue(nodeId, colIndex, n -> ((TreeListNodeResource) n).cellBGColors,
+               new ColorRgb(color.intValue()));
    }
 
    /**
@@ -975,7 +982,8 @@
       
       if (buildingTree <= 0)
       {
-         setAttr("columns", config.columns, columns, v -> config.columns = new ArrayList(columns));
+         setAttr("columns", ((TreeListConfig) config).columns, columns,
+                  v -> ((TreeListConfig) config).columns = new ArrayList(columns));
          pendingPushColumns = false;
       }
       else
@@ -997,12 +1005,12 @@
     * @return  new node instance.
     */
    @Override
-   protected TreeListNodeResource<N> createNode(final String key,
-                                                final String text, 
-                                                final TreeListNodeResource<N> parent)
+   protected TreeListNodeResource createNode(final String key,
+                                             final String text, 
+                                             final TreeNodeFace parent)
    {
       final int nodeId = getNextNodeId();
-      return new TreeListNodeResource<N>(this, nodeId, key, text, parent);
+      return new TreeListNodeResource(this, nodeId, key, text, parent);
    }
 
    /**
@@ -1019,7 +1027,8 @@
 
       if (buildingTree <= 0)
       {
-         setAttr("columns", config.columns, columns, v -> config.columns = new ArrayList(v));
+         setAttr("columns", ((TreeListConfig) config).columns, columns,
+                  v -> ((TreeListConfig) config).columns = new ArrayList(v));
          pendingPushColumns = false;
       }
       else
@@ -1074,9 +1083,9 @@
     * @return  Reference to node entry object or {@code null} if not found.
     */
    @SuppressWarnings("unused")
-   private TreeListNodeEntry findListNodeEntry(NumberType nodeId)
+   private TreeNodeEntry findListNodeEntry(NumberType nodeId)
    {
-      for (TreeListNodeEntry n : getAttr("nodes", () -> config.nodes))
+      for (TreeNodeEntry n : getAttr("nodes", () -> ((TreeListConfig)config).nodes))
       {
          if (n.nodeId == nodeId.intValue())
          {
@@ -1103,7 +1112,7 @@
     */
    private final <T> void setCellValue(final NumberType nodeId,
                                        final NumberType colIndex,
-                                       final Function<TreeListNodeResource<N>, List<T>> getValues,
+                                       final Function<TreeNodeFace, List<T>> getValues,
                                        final T newValue)
    {
       if (!assertKnown(nodeId, colIndex))
@@ -1111,7 +1120,7 @@
          return;
       }
 
-      final TreeListNodeResource<N> n = findNode(nodeId.intValue());
+      final TreeNodeFace n = findNode(nodeId.intValue());
       if (n == null)
       {
          return;
@@ -1161,20 +1170,20 @@
     */
    private final <T> T getCellValue(final NumberType nodeId,
                                     final NumberType colIndex,
-                                    final Function<TreeListNodeResource<N>, List<T>> getValues)
+                                    final Function<TreeNodeFace, List<T>> getValues)
    {
       if (!assertKnown(nodeId, colIndex))
       {
          return null;
       }
 
-      final TreeListNodeResource<N> n = findNode(nodeId.intValue());
+      final TreeNodeFace n = findNode(nodeId.intValue());
       if (n == null)
       {
          return null;
       }
 
-      return n.getCellValue(colIndex.intValue(), getValues);
+      return ((TreeListNodeResource)n).getCellValue(colIndex.intValue(), getValues);
    }
 
    /**

=== modified file 'src/com/goldencode/p2j/ui/TreeNodeCollection.java'
--- src/com/goldencode/p2j/ui/TreeNodeCollection.java	2020-09-14 13:46:01 +0000
+++ src/com/goldencode/p2j/ui/TreeNodeCollection.java	2021-06-17 16:27:23 +0000
@@ -77,9 +77,9 @@
  *        tree node face type
  */
 @LegacyResource(resource = LegacyResource.TREE_NODE_COLLECTION)
-public interface TreeNodeCollection<T extends TreeNodeFace>
+public interface TreeNodeCollection
 extends WrappedResource,
-        Iterable<T>,
+        Iterable<TreeNodeFace>,
         RemoveNode
 {
    /**

=== modified file 'src/com/goldencode/p2j/ui/TreeNodeCollectionResource.java'
--- src/com/goldencode/p2j/ui/TreeNodeCollectionResource.java	2021-02-09 09:35:10 +0000
+++ src/com/goldencode/p2j/ui/TreeNodeCollectionResource.java	2021-06-17 17:07:28 +0000
@@ -91,14 +91,12 @@
  *        node type
  * @param <C> 
  *        config type
- * @param <R> 
+ * @param  <TreeNodeResource>  
  *        resource type
 */
-public class TreeNodeCollectionResource<N extends TreeNodeEntry,
-                                        C extends TreeConfig<N>,
-                                        R extends TreeNodeResource<N>>
+public class TreeNodeCollectionResource
 extends HandleResource
-implements TreeNodeCollection<R>,
+implements TreeNodeCollection,
            DynamicResource
 {
    /** Represents the default key and text values for a new created tree node. */
@@ -112,13 +110,13 @@
    private final static int CHILD_NODE_RELATIONSHIP    = 4;
 
    /** The nodes collection implementation. */
-   private LinkedList<R> nodes = new LinkedList<>();
+   private LinkedList<TreeNodeFace> nodes = new LinkedList<>();
 
    /** The owning tree. */
-   private TreeWidgetBase<N, C, R> tree;
+   private TreeWidgetBase tree;
 
    /** The owning tree node. */
-   private R owner;
+   private TreeNodeResource owner;
 
    /**
     * Constructor.
@@ -128,7 +126,7 @@
     * @param   owner
     *          The owning node.
     */
-   public TreeNodeCollectionResource(TreeWidgetBase<N,C,R> tree, R owner)
+   public TreeNodeCollectionResource(final TreeWidgetBase tree, TreeNodeResource owner)
    {
       super(tree._dynamic());
       this.tree = tree;
@@ -141,7 +139,7 @@
     * @return  an Iterator.
     */
    @Override
-   public ListIterator<R> iterator()
+   public ListIterator<TreeNodeFace>  iterator()
    {
       return nodes == null ? Collections.emptyListIterator() : nodes.listIterator();
    }
@@ -155,9 +153,9 @@
     *
     * @return  an Iterator.
     */
-   public Iterator<R> recursiveIterator(boolean visibleOnly)
+   public Iterator <TreeNodeFace>  recursiveIterator(boolean visibleOnly)
    {
-      return nodes == null ? Collections.emptyIterator() : new NodesIterator<>(nodes.listIterator(),
+      return nodes == null ? Collections.emptyIterator() : new NodesIterator(nodes.listIterator(),
                                                                              visibleOnly);
    }
 
@@ -208,7 +206,7 @@
    @Override
    public handle addNextNode(handle relativeNode, character key, character text)
    {
-      return _addNodeImpl((R) relativeNode.getResource(), Location.NEXT, key, text, true);
+      return _addNodeImpl((TreeNodeFace)relativeNode.getResource(), Location.NEXT, key, text, true);
    }
 
    /**
@@ -226,7 +224,7 @@
    @Override
    public handle addChildNode(handle relativeNode, character key, character text)
    {
-      return _addNodeImpl((R) relativeNode.getResource(), Location.CHILD, key, text, true);
+      return _addNodeImpl((TreeNodeFace) relativeNode.getResource(), Location.CHILD, key, text, true);
    }
 
    /**
@@ -255,7 +253,7 @@
          return;
       }
 
-      R node = tree.findNode(key.toStringMessage());
+      TreeNodeFace node = tree.findNode(key.toStringMessage());
       _removeNode(node, null);
       tree.pushNodes();
    }
@@ -271,7 +269,7 @@
          return;
       }
 
-      ListIterator<R> it = nodes.listIterator();
+      final ListIterator <TreeNodeFace>  it = nodes.listIterator();
       while (it.hasNext())
       {
          _removeNode(it.next(), it);
@@ -358,7 +356,7 @@
       
       int index = nodeIndex.intValue();
    // the internal root node has a zero index, indices of none root nodes are 1-based
-      R node = tree.getTreeNodeByIndex(index);
+      TreeNodeFace node = tree.getTreeNodeByIndex(index);
       
       if (node == null)
       {
@@ -392,7 +390,7 @@
       
       int index = nodeIndex.intValue();
       // the internal root node has a zero index, indices of none root nodes are 1-based
-      R node = tree.getTreeNodeByIndex(index);
+      TreeNodeFace node = tree.getTreeNodeByIndex(index);
       
       if (node == null)
       {
@@ -482,8 +480,8 @@
                      character key,
                      character text)
    {
-      R relative;
-      TreeNodeCollectionResource<N, C, R> nodesCollection;
+      TreeNodeFace relative;
+      TreeNodeCollectionResource nodesCollection;
       if (anchorIndex == null || anchorIndex.isUnknown())
       {
          relative = null;
@@ -504,7 +502,7 @@
             return new handle();
          }
          
-         nodesCollection = relative.parent._getNodes();
+         nodesCollection = ((TreeNodeResource)((TreeNodeResource)relative).parent)._getNodes();
       }
       
       Location location = Location.valueOf(relationType.intValue());
@@ -516,7 +514,7 @@
          return new handle();
       }
       
-      R treeNode = tree.findNode(key.getValue());
+      TreeNodeFace treeNode = tree.findNode(key.getValue());
       
       if (treeNode != null)
       {
@@ -596,12 +594,12 @@
       
       super.delete();
       
-      TreeWidgetBase<N,C,R> tree = this.tree;
+      TreeWidgetBase tree = this.tree;
       this.tree = null;
 
       if (nodes != null)
       {
-         ListIterator<R> nodeIt = nodes.listIterator();
+         ListIterator<TreeNodeFace>  nodeIt = nodes.listIterator();
          while (nodeIt.hasNext())
          {
             _removeNode(nodeIt.next(), nodeIt);
@@ -619,19 +617,19 @@
    @Override
    public integer getCount()
    {
-      ListIterator<R> iter = iterator();
+      ListIterator<TreeNodeFace>  iter = iterator();
       
       int collect = 0;
       
-      R rootNode = tree.getTreeNodeByIndex(0);
+      TreeNodeFace rootNode = tree.getTreeNodeByIndex(0);
       
       while(iter.hasNext())
       {
-         R node = iter.next();
+         TreeNodeFace node = iter.next();
          
-         if (node._getHasChildren())
+         if (((TreeNodeResource)node)._getHasChildren())
          {
-            collect += node._getNodes().getCount().intValue();
+            collect += ((TreeNodeResource)node)._getNodes().getCount().intValue();
          }
          
          // don't take into account the internal root node
@@ -670,7 +668,7 @@
     * @param   it
     *          Optional iterator. When set, use it to remove the item from its list.
     */
-   void _removeNode(R node, ListIterator<R> it)
+   void _removeNode(final TreeNodeFace node, ListIterator <TreeNodeFace> it)
    {
       if (node == null || nodes == null)
       {
@@ -686,10 +684,10 @@
          return;
       }
 
-      node._removeCollection();
+      ((TreeNodeResource)node)._removeCollection();
 
       // resource cleanup
-      node.delete();
+      ((TreeNodeResource)node).delete();
    }
 
    /**
@@ -711,7 +709,7 @@
     *
     * @return  see above.
     */
-   int _indexOf(R node)
+   int _indexOf(final TreeNodeFace node)
    {
       return nodes == null ? -1 : nodes.indexOf(node);
    }
@@ -726,7 +724,7 @@
     * @throws  IndexOutOfBoundsException
     *          if the index is out of range (<tt>index &lt; 0 || index &gt;= size()</tt>)
     */
-   R _getNode(int index)
+   TreeNodeFace _getNode(int index)
    {
       if (nodes == null)
       {
@@ -753,7 +751,7 @@
     *
     * @return  handle with wrapped {@link TreeNodeResource} instance, or unknown if the operation fails.
     */
-   handle _addNodeImpl(R relative,
+   handle _addNodeImpl(TreeNodeFace relative,
                        Location location,
                        character key,
                        character text,
@@ -769,7 +767,7 @@
          text = key;
       }
 
-      R parent = owner;
+      TreeNodeFace parent = owner;
 
       if (location == Location.CHILD)
       {
@@ -786,7 +784,7 @@
          nodes = new LinkedList<>();
       }
       
-      R node = createNode(key.toStringMessage(), text.toStringMessage(), parent);
+      TreeNodeFace node = createNode(key.toStringMessage(), text.toStringMessage(), parent);
 
       switch (location)
       {
@@ -835,11 +833,11 @@
             }
             break;
          case CHILD:
-            parent._getNodes().nodes.addLast(node);
+            ((TreeNodeResource)parent)._getNodes().nodes.addLast(node);
             break;
       }
 
-      parent._setHasChildren(true);
+      ((TreeNodeResource)parent)._setHasChildren(true);
       tree.addNode(node, pushNodes);
       return new handle(node);
    }
@@ -861,7 +859,7 @@
     * @throws  RuntimeException
     *          when the supplied key is not unique
     */
-   private R createNode(String key, String text, R parent)
+   private TreeNodeFace createNode(String key, String text, final TreeNodeFace parent)
    {
       if (key == null || text == null || parent == null)
       {
@@ -874,11 +872,11 @@
    /**
     * Recursive nodes iterator.
     */
-   private static class NodesIterator<N extends TreeNodeEntry,T extends TreeNodeResource<N>>
-   implements Iterator<T>
+   private static class NodesIterator
+   implements Iterator<TreeNodeFace>
    {
       /** Stack of iterators to handle recursion. */
-      private Stack<ListIterator<T>> stack = new Stack<>();
+      private Stack<ListIterator<TreeNodeFace>> stack = new Stack<>();
 
       /** All or only expanded nodes to be iterated */
       private boolean visibleOnly;
@@ -888,25 +886,14 @@
        *
        * @param   node
        *          The parent node to iterate.
-       */
-      public NodesIterator(T node)
-      {
-         this(node, true);
-      }
-
-      /**
-       * Constructor.
-       *
-       * @param   node
-       *          The parent node to iterate.
        * @param   visibleOnly
        *          All or only expanded nodes to be iterated.
        */
-      public NodesIterator(T node, boolean visibleOnly)
+      public NodesIterator(final TreeNodeFace node, boolean visibleOnly)
       {
          if (node != null && node.isNodeExpanded().booleanValue())
          {
-            LinkedList<T> nodes = (LinkedList<T>)node._getNodes().nodes;
+            LinkedList<TreeNodeFace> nodes = (LinkedList<TreeNodeFace>)((TreeNodeResource)node)._getNodes().nodes;
             stack.push(nodes == null ? Collections.emptyListIterator() : nodes.listIterator());
          }
 
@@ -921,7 +908,7 @@
        * @param   visibleOnly
        *          All or only expanded nodes to be iterated.
        */
-      public NodesIterator(ListIterator<T> nodes, boolean visibleOnly)
+      public NodesIterator(ListIterator<TreeNodeFace> nodes, boolean visibleOnly)
       {
          stack.push(nodes);
          this.visibleOnly = visibleOnly;
@@ -937,7 +924,7 @@
       @Override
       public boolean hasNext()
       {
-         for (Iterator<T> it : stack)
+         for (ListIterator<TreeNodeFace> it : stack)
          {
             if (it.hasNext())
             {
@@ -955,7 +942,7 @@
        * @throws NoSuchElementException if the iteration has no more elements
        */
       @Override
-      public T next()
+      public TreeNodeFace next()
       {
          if (stack.isEmpty())
          {
@@ -967,27 +954,27 @@
             stack.pop();
          }
 
-         Iterator<T> it = stack.isEmpty() ? null : stack.peek();
+         Iterator<TreeNodeFace> it = stack.isEmpty() ? null : stack.peek();
 
          if (it == null || !it.hasNext())
          {
             throw new NoSuchElementException();
          }
 
-         TreeNodeResource<N> node = it.next();
+         TreeNodeFace node = it.next();
          if (!visibleOnly || node.isNodeExpanded().booleanValue())
          {
-            TreeNodeCollectionResource<N, ?, ?> coll = node._getNodes(false);
+            TreeNodeCollectionResource coll = ((TreeNodeResource)node)._getNodes(false);
             if (coll != null)
             {
                if (coll.nodes != null && !coll.nodes.isEmpty())
                {
-                  stack.push((ListIterator<T>)coll.nodes.listIterator());
+                  stack.push((ListIterator<TreeNodeFace>)coll.nodes.listIterator());
                }
             }
          }
 
-         return (T) node;
+         return node;
       }
    }
 

=== modified file 'src/com/goldencode/p2j/ui/TreeNodeEntry.java'
--- src/com/goldencode/p2j/ui/TreeNodeEntry.java	2021-01-28 21:48:33 +0000
+++ src/com/goldencode/p2j/ui/TreeNodeEntry.java	2021-06-17 17:51:29 +0000
@@ -74,13 +74,17 @@
 import java.io.*;
 import java.util.*;
 
+import com.goldencode.util.*;
+import com.google.common.base.MoreObjects.*;
+
 /**
  * Treeview node configuration.
  */
 public abstract class TreeNodeEntry
+extends AbstractPrintable
 implements Externalizable
 {
-   /** Cell tvalue types */
+   /** Cell tvalue types used for cell value serialization */
    private static final byte NULL = 0;
    private static final byte TEXT = 1;
    private static final byte NUMBER = 2;
@@ -124,6 +128,7 @@
     */
    public TreeNodeEntry()
    {
+      // no-op
    }
 
    /**
@@ -134,7 +139,7 @@
     * @param   parentId
     *          Parent node id. Use unknown when the node has no parent.
     */
-   public TreeNodeEntry(int nodeId, int parentId)
+   public TreeNodeEntry(final int nodeId, final int parentId)
    {
       this.nodeId = nodeId;
       this.parentId = parentId;
@@ -147,6 +152,20 @@
     * @param   cellIndex
     *          Cell index.
     *
+    * @return  cell value or {@code null} if the index is out of range of valid indexes
+    */
+   public Object getValue()
+   {
+      return getValue(0);
+   }
+
+   /**
+    * Returns the cell value at the specified index. The index is the index to the data model, not the
+    * actual (column) position.
+    *
+    * @param   cellIndex
+    *          Cell index.
+    *
     * @return  cell value.
     *
     * @throws  IndexOutOfBoundsException
@@ -283,14 +302,15 @@
    }
 
    /**
-    * Write cell value
+    * Write cell value.
+    * 
     * @param out
     *        output stream
     * @param value
     *        value to be written
     * @throws IOException
     */
-   protected void writeCell(ObjectOutput out, Object value) throws IOException
+   protected static void writeCell(ObjectOutput out, Object value) throws IOException
    {
       if (value == null)
       {
@@ -322,7 +342,7 @@
     * @throws IOException
     * @throws ClassNotFoundException 
     */
-   protected Object readCell(ObjectInput in) throws IOException, ClassNotFoundException
+   protected static Object readCell(ObjectInput in) throws IOException, ClassNotFoundException
    {
       byte type = in.readByte();
       switch(type)
@@ -352,4 +372,35 @@
       /** */
       INACTIVE
    }
+
+   /**
+   * Add class-specific fields to the helper.
+   *  
+   * @param stringHelper the string helper to update
+   */
+   @Override
+   public void updateStringHelper(ToStringHelper stringHelper)
+   {
+      stringHelper.add("id", nodeId);
+
+      if (parentId >= 0)
+      {
+         stringHelper.add("parentId", parentId);
+      }
+
+      if (hasChildren)
+      {
+         stringHelper.addValue("may have children");
+      }
+
+      if (expanded)
+      {
+         stringHelper.addValue("expanded");
+      }
+
+      if (tooltip != null)
+      {
+         stringHelper.add("tooltip", tooltip);
+      }
+   }
 }

=== modified file 'src/com/goldencode/p2j/ui/TreeNodeResource.java'
--- src/com/goldencode/p2j/ui/TreeNodeResource.java	2021-06-01 18:55:22 +0000
+++ src/com/goldencode/p2j/ui/TreeNodeResource.java	2021-06-17 17:24:53 +0000
@@ -101,7 +101,7 @@
  * @param <N> 
  *        node type
  */
-public abstract class TreeNodeResource<N extends TreeNodeEntry>
+public abstract class TreeNodeResource
 extends HandleResource
 implements TreeNodeFace,
            DynamicResource
@@ -140,13 +140,13 @@
    protected String key;
 
    /** The owning tree view. */
-   protected TreeWidgetBase<N, TreeConfig<N>, TreeNodeResource<N>> tree;
+   protected TreeWidgetBase tree;
 
    /** Node parent */
-   protected TreeNodeResource<N> parent;
+   protected TreeNodeFace parent;
 
    /** Child nodes */
-   protected TreeNodeCollectionResource<N, TreeConfig<N>, TreeNodeResource<N>> nodes;
+   protected TreeNodeCollectionResource nodes;
 
    /** Node tooltip */
    private String tooltip;
@@ -173,16 +173,13 @@
     * @param   parent
     *          Node parent.
     */
-   public <R extends TreeNodeResource<N>,
-           C extends TreeConfig<N>,
-           W extends TreeWidgetBase<N, C, R>> 
-          TreeNodeResource(W tree,
+   public TreeNodeResource(TreeWidgetBase tree,
                            int nodeId,
                            String key,
-                           R parent)
+                           TreeNodeFace parent)
    {
       super(tree._dynamic());
-      this.tree = (TreeWidgetBase<N, TreeConfig<N>, TreeNodeResource<N>>) tree;
+      this.tree = (TreeWidgetBase) tree;
       this.nodeId = nodeId;
       this.key = key;
       this.parent = parent;
@@ -239,7 +236,7 @@
          return;
       }
       
-      TreeNodeResource<N> treeNode = tree.findNode(newKeyValue);
+      TreeNodeFace treeNode = tree.findNode(newKeyValue);
       
       if (treeNode != null)
       {
@@ -399,7 +396,7 @@
          return new integer();
       }
 
-      int i = parent.nodes._indexOf(this);
+      int i = ((TreeNodeResource)parent).nodes._indexOf(this);
       return i < 0 ? new integer() : new integer(i);
    }
 
@@ -673,7 +670,7 @@
    {
       LinkedList<String> stack = new LinkedList<>();
       
-      TreeNodeResource<N> current = this;
+      TreeNodeFace current = this;
       
       while (current != null)
       {
@@ -692,7 +689,7 @@
          
          stack.add(label);
          
-         current = current.parent;
+         current = ((TreeNodeResource)current).parent;
       }
       
       StringBuilder pathCollector = new StringBuilder();
@@ -828,7 +825,7 @@
    @Override
    public handle getPreviousNode()
    {
-      TreeNodeCollectionResource<N, TreeConfig<N>, TreeNodeResource<N>> siblings = parent.nodes;
+      TreeNodeCollectionResource siblings = ((TreeNodeResource)parent).nodes;
       int index = siblings._indexOf(this);
       
       if (index > 0)
@@ -847,9 +844,9 @@
    @Override
    public handle getNextNode()
    {
-      TreeNodeCollectionResource<N, TreeConfig<N>, TreeNodeResource<N>> siblings = parent.nodes;
-      int index = siblings._indexOf(this);
-      int limit = siblings._getCount() - 1;
+      final TreeNodeCollectionResource siblings = ((TreeNodeResource)parent).nodes;
+      final int index = siblings._indexOf(this);
+      final int limit = siblings._getCount() - 1;
       
       if (index >= 0 && index < limit)
       {
@@ -867,7 +864,7 @@
    @Override
    public handle getFirstSiblingNode()
    {
-      TreeNodeCollectionResource<N, TreeConfig<N>, TreeNodeResource<N>> siblings = parent.nodes;
+      final TreeNodeCollectionResource siblings = ((TreeNodeResource)parent).nodes;
       
       if (siblings != null)
       {
@@ -885,7 +882,7 @@
    @Override
    public handle getLastSiblingNode()
    {
-      TreeNodeCollectionResource<N, TreeConfig<N>, TreeNodeResource<N>> siblings = parent.nodes;
+      final TreeNodeCollectionResource siblings = ((TreeNodeResource)parent).nodes;
       
       if (siblings != null)
       {
@@ -914,7 +911,7 @@
 
       super.delete();
       
-      TreeWidgetBase<N, TreeConfig<N>, TreeNodeResource<N>> tree = this.tree;
+      TreeWidgetBase tree = this.tree;
       this.tree = null;
 
       if (nodeId == 0)
@@ -923,7 +920,7 @@
       }
       else
       {
-         parent._getNodes()._removeNode(this, null);
+         ((TreeNodeResource)parent)._getNodes()._removeNode(this, null);
       }
 
       tree.removeNode(this, true);
@@ -960,7 +957,7 @@
     *
     * @return  new instance of {@link TreeNodeEntry}.
     */
-   protected abstract N saveToEntry();
+   protected abstract TreeNodeEntry saveToEntry();
 
    /**
     * The method serializes the node state into the supplied entry instance.
@@ -987,7 +984,7 @@
     * @param   entry
     *          A valid {@link TreeNodeEntry} instance.
     */
-   protected void loadFromEntry(N entry)
+   protected void loadFromEntry(final TreeNodeEntry entry)
    {
       expanded = entry.expanded;
       
@@ -999,8 +996,7 @@
     *
     * @return  see above.
     */
-   <C extends TreeConfig<N>, R extends TreeNodeResource<N>>   
-   TreeNodeCollectionResource<N, C, R> _getNodes()
+   TreeNodeCollectionResource _getNodes()
    {
       return _getNodes(true);
    }
@@ -1013,15 +1009,14 @@
     *
     * @return  see above.
     */
-   <C extends TreeConfig<N>, R extends TreeNodeResource<N>>   
-   TreeNodeCollectionResource<N, C, R> _getNodes(boolean allocate)
+   TreeNodeCollectionResource _getNodes(boolean allocate)
    {
       if (nodes == null && allocate)
       {
-         nodes = new TreeNodeCollectionResource<N, TreeConfig<N>, TreeNodeResource<N>>(tree, this);
+         nodes = new TreeNodeCollectionResource(tree, this);
       }
 
-      return (TreeNodeCollectionResource<N, C, R>)nodes;
+      return (TreeNodeCollectionResource)nodes;
    }
 
    /**
@@ -1055,8 +1050,8 @@
    int _getNodeLevel()
    {
       int level = -1;
-      TreeNodeResource<N> p = this;
-      while ((p = p.parent) != null)
+      TreeNodeFace p = this;
+      while ((p = ((TreeNodeResource)p).parent) != null)
       {
          level++;
       }
@@ -1069,7 +1064,7 @@
     *
     * @return  parent node.
     */
-   TreeNodeResource<N> _getParent()
+   TreeNodeFace _getParent()
    {
       return parent;
    }
@@ -1173,11 +1168,11 @@
    {
       if (expanded)
       {
-         TreeNodeResource<N> pNode = parent;
-         while (pNode != null && !pNode.expanded)
+         TreeNodeFace pNode = parent;
+         while (pNode != null && !((TreeNodeResource)pNode).expanded)
          {
-            pNode.expanded = true;
-            pNode = pNode.parent;
+            ((TreeNodeResource)pNode).expanded = true;
+            pNode = ((TreeNodeResource)pNode).parent;
          }
       }
 

=== modified file 'src/com/goldencode/p2j/ui/TreeView.java'
--- src/com/goldencode/p2j/ui/TreeView.java	2019-03-14 00:27:05 +0000
+++ src/com/goldencode/p2j/ui/TreeView.java	2021-06-17 17:52:14 +0000
@@ -81,4 +81,5 @@
 extends WrappedResource,
         TreeFace
 {
+   // empty
 }

=== modified file 'src/com/goldencode/p2j/ui/TreeViewConfig.java' (properties changed: +x to -x)
--- src/com/goldencode/p2j/ui/TreeViewConfig.java	2020-09-14 13:46:01 +0000
+++ src/com/goldencode/p2j/ui/TreeViewConfig.java	2021-06-17 18:05:43 +0000
@@ -74,7 +74,7 @@
  * TreeView widget configuration.
  */
 public class TreeViewConfig
-extends TreeConfig<TreeViewNodeEntry>
+extends TreeConfig
 {
    /**
     * Default constructor.
@@ -111,7 +111,7 @@
     * @return node entry instance
     */
    @Override
-   protected  Supplier<TreeViewNodeEntry> createNodeEntry()
+   protected  Supplier<TreeNodeEntry> createNodeEntry()
    {
       return TreeViewNodeEntry::new;
    }

=== modified file 'src/com/goldencode/p2j/ui/TreeViewNode.java'
--- src/com/goldencode/p2j/ui/TreeViewNode.java	2019-03-14 00:27:05 +0000
+++ src/com/goldencode/p2j/ui/TreeViewNode.java	2021-06-17 17:51:55 +0000
@@ -77,4 +77,5 @@
 public interface TreeViewNode
 extends TreeNodeFace
 {
+   // empty
 }

=== modified file 'src/com/goldencode/p2j/ui/TreeViewNodeEntry.java'
--- src/com/goldencode/p2j/ui/TreeViewNodeEntry.java	2021-01-29 14:13:08 +0000
+++ src/com/goldencode/p2j/ui/TreeViewNodeEntry.java	2021-06-16 19:56:27 +0000
@@ -78,7 +78,7 @@
 extends TreeNodeEntry
 {
    /** Node text. */
-   public String text = null;
+   /* private */ public String text = null;
 
    /**
     * Constructor.
@@ -97,7 +97,7 @@
     * @param   text
     *          Node label.
     */
-   public TreeViewNodeEntry(int nodeId, int parentId, String text)
+   public TreeViewNodeEntry(final int nodeId, final int parentId, final String text)
    {
       super(nodeId, parentId);
       this.text = text;
@@ -106,17 +106,14 @@
    /**
     * Returns the cell value at the specified index. The index is the index to the data model, not the
     * actual (column) position.
-    * <p>
-    * For TREEVIEW widget the index is ignored.
-    *
     *
     * @param   cellIndex
     *          Cell index.
     *
-    * @return  cell value.
+    * @return  cell value or {@code null} if the index is out of range of valid indexes
     */
    @Override
-   public Object getValue(int cellIndex)
+   public Object getValue(final int cellIndex)
    {
       return text;
    }

=== modified file 'src/com/goldencode/p2j/ui/TreeViewNodeResource.java'
--- src/com/goldencode/p2j/ui/TreeViewNodeResource.java	2020-09-07 16:23:31 +0000
+++ src/com/goldencode/p2j/ui/TreeViewNodeResource.java	2021-06-17 17:38:55 +0000
@@ -75,7 +75,7 @@
  * TREEVIEW-NODE resource implementation.
  */
 public class TreeViewNodeResource
-extends TreeNodeResource<TreeViewNodeEntry>
+extends TreeNodeResource
 implements TreeViewNode
 {
    private String text;
@@ -94,11 +94,11 @@
     * @param   parent
     *          Node parent.
     */
-   public TreeViewNodeResource(TreeViewWidget treeView,
+   public TreeViewNodeResource(TreeWidgetBase treeView,
                                int nodeId,
                                String key,
                                String text,
-                               TreeViewNodeResource parent)
+                               TreeNodeFace parent)
    {
       super(treeView, nodeId, key, parent);
       this.text = text;
@@ -136,9 +136,10 @@
     *
     * @return  new instance of {@link TreeViewNodeEntry}.
     */
+   @Override
    protected TreeViewNodeEntry saveToEntry()
    {
-      int parentId = parent != null ? parent.nodeId : Integer.MIN_VALUE;
+      int parentId = parent != null ? ((TreeNodeResource)parent).nodeId : Integer.MIN_VALUE;
       String effectiveText = text == null ? key : text;
       TreeViewNodeEntry entry = new TreeViewNodeEntry(nodeId, parentId, effectiveText);
       super.saveToEntry(entry);
@@ -151,10 +152,11 @@
     * @param   entry
     *          A valid {@link TreeNodeEntry} instance.
     */
-   protected void loadFromEntry(TreeViewNodeEntry entry)
+   @Override
+   protected void loadFromEntry(final TreeNodeEntry entry)
    {
       super.loadFromEntry(entry);
       
-      text = entry.text;
+      text = entry.getValue().toString();
    }
 }

=== modified file 'src/com/goldencode/p2j/ui/TreeViewWidget.java'
--- src/com/goldencode/p2j/ui/TreeViewWidget.java	2021-06-01 18:55:22 +0000
+++ src/com/goldencode/p2j/ui/TreeViewWidget.java	2021-06-17 17:20:26 +0000
@@ -79,7 +79,7 @@
  * Server side TreeView widget implementation.
  */
 public class TreeViewWidget
-extends TreeWidgetBase<TreeViewNodeEntry, TreeViewConfig, TreeViewNodeResource>
+extends TreeWidgetBase
 implements TreeView
 {
    /**
@@ -114,7 +114,7 @@
     * @return  new node instance.
     */
    @Override
-   protected TreeViewNodeResource createNode(String key, String text, TreeViewNodeResource parent)
+   protected TreeNodeFace createNode(String key, String text, TreeNodeFace parent)
    {
       int nodeId = getNextNodeId();
       return new TreeViewNodeResource(this, nodeId, key, text, parent);

=== modified file 'src/com/goldencode/p2j/ui/TreeWidgetBase.java' (properties changed: +x to -x)
--- src/com/goldencode/p2j/ui/TreeWidgetBase.java	2021-06-01 18:55:22 +0000
+++ src/com/goldencode/p2j/ui/TreeWidgetBase.java	2021-06-17 18:05:43 +0000
@@ -131,17 +131,9 @@
 
 /**
  * Server side Tree widget base implementation.
- * @param <N> 
- *        node type
- * @param <C> 
- *        config type
- * @param <R> 
- *        resource type
  */
-public abstract class TreeWidgetBase<N extends TreeNodeEntry,
-                                     C extends TreeConfig<N>,
-                                     R extends TreeNodeResource<N>>
-extends ControlEntityExt<C>
+public abstract class TreeWidgetBase
+extends ControlEntityExt<TreeConfig>
 implements TreeFace
 {
    /**
@@ -152,22 +144,22 @@
    /**
     * Map of nodes by ids.
     */
-   private HashMap<Integer, R> nodesById = new HashMap<>();
+   private HashMap<Integer, TreeNodeFace > nodesById = new HashMap<>();
 
    /**
     * Map of nodes by keys.
     */
-   private HashMap<String, R> nodesByKey = new HashMap<>();
+   private HashMap<String, TreeNodeFace > nodesByKey = new HashMap<>();
 
    /**
     * The list of all tree nodes.
     */
-   private LinkedList<R> nodesByIndices = new LinkedList<>();
+   private LinkedList<TreeNodeFace > nodesByIndices = new LinkedList<>();
 
    /**
     * The root node.
     */
-   private R rootNode;
+   private TreeNodeFace rootNode;
 
    /**
     * The backed field for BUILDING-TREE attribute.
@@ -178,10 +170,10 @@
    private boolean pendingPushNodes;
 
    /** Widget attributes pending to be sent to the client */
-   private ArrayList<String> pendingAttrs = new ArrayList<>();
+   private List<String> pendingAttrs = new ArrayList<>();
 
    /** Widget attribute values pending to be sent to the client */
-   private ArrayList<Object> pendingAttrValues = new ArrayList<>();
+   private List<Object> pendingAttrValues = new ArrayList<>();
 
    /** Assigned imagelist for node images. */
    private handle nodeImageListHandle;
@@ -197,7 +189,7 @@
     * @param    ctrl
     *           Configuration container.
     */
-   public TreeWidgetBase(boolean dynamic, C ctrl)
+   public TreeWidgetBase(boolean dynamic, TreeConfig ctrl)
    {
       super(dynamic, ctrl);
       rootNode = createNode("", "", null);
@@ -218,14 +210,14 @@
     *          the owner's config was modified.
     */
    @Override
-   public void afterConfigUpdate(C beforeUpdate)
+   public void afterConfigUpdate(TreeConfig beforeUpdate)
    {
-      for (N e : getAttr("nodes", () -> config.nodes))
+      for (TreeNodeEntry e : getAttr("nodes", () -> config.nodes))
       {
-         TreeNodeResource<N> node = nodesById.get(e.nodeId);
+         TreeNodeFace node = nodesById.get(e.nodeId);
          if (node != null)
          {
-            node.loadFromEntry(e);
+            ((TreeNodeResource)node).loadFromEntry(e);
          }
       }
 
@@ -350,7 +342,7 @@
    public handle getDraggedNode()
    {
       int nodeId = getAttr("draggedNode", () -> config.draggedNode);
-      R node = findNode(nodeId);
+      TreeNodeFace node = findNode(nodeId);
       return new handle(node);
    }
 
@@ -363,7 +355,7 @@
    public handle getDraggedOverNode()
    {
       int nodeId = getAttr("draggedOverNode", () -> config.draggedOverNode);
-      R node = findNode(nodeId);
+      TreeNodeFace node = findNode(nodeId);
       return new handle(node);
    }
 
@@ -381,7 +373,8 @@
          return new handle();
       }
       
-      TreeNodeResource<N> node = nodesById.get(getAttr("dropHighlightNodeId", () -> config.dropHighlightNodeId));
+      TreeNodeFace node = nodesById
+               .get(getAttr("dropHighlightNodeId", () -> config.dropHighlightNodeId));
       
       return new handle(node);
    }
@@ -420,7 +413,7 @@
    /**
     * Getter for the SELECTED-NODE attribute.
     * The method returns a handle the currently selected node or tree root node handle when no node
-    * is currently selected. The handle will wrap an instance of {@link TreeNodeFace}.
+    * is currently selected. The handle will wrap an instance of {@link TreeNodeFace }.
     *
     * @return  the attribute value.
     */
@@ -523,7 +516,7 @@
          return new character();
       }
 
-      return ((TreeNodeResource<N>) node.getResource()).getNodeKey();
+      return ((TreeNodeResource) node.getResource()).getNodeKey();
    }
 
    /**
@@ -557,7 +550,7 @@
          return new character();
       }
 
-      return ((TreeNodeResource<N>) node.getResource()).getNodeKey();
+      return ((TreeNodeResource) node.getResource()).getNodeKey();
    }
 
    /**
@@ -569,7 +562,7 @@
    @Override
    public handle getNodes()
    {
-      return rootNode.getNodes();
+      return ((TreeNodeResource)rootNode).getNodes();
    }
 
    /**
@@ -614,7 +607,7 @@
    public integer getVisibleNodeCount()
    {
       int count = 0;
-      Iterator<?> it = rootNode._getNodes().recursiveIterator(true);
+      Iterator<?> it = ((TreeNodeResource)rootNode)._getNodes().recursiveIterator(true);
       while(it.hasNext())
       {
          it.next();
@@ -633,13 +626,13 @@
    @Override
    public handle getFirstVisibleNode()
    {
-      int nodeId = getAttr("topNode", () -> config.topNode, true);
+      int nodeId = _getTopVisibleNodeId();
       if (nodeId <= 0)
       {
          return new handle(rootNode);
       }
 
-      R node = findNode(nodeId);
+       TreeNodeFace node = findNode(nodeId);
       return new handle(node);
    }
 
@@ -651,8 +644,8 @@
    @Override
    public integer getFirstNode()
    {
-      TreeNodeResource<N> node = rootNode._getNodes()._getNode(0);
-      return node == null ? new integer() : new integer(node._getNodeId());
+       TreeNodeFace node = ((TreeNodeResource)rootNode)._getNodes()._getNode(0);
+      return node == null ? new integer() : new integer(((TreeNodeResource)node)._getNodeId());
    }
 
    /**
@@ -665,16 +658,16 @@
    public integer getNextNode(NumberType nodeId)
    {
       boolean found = false;
-      Iterator<TreeNodeResource<N>> it = rootNode._getNodes().recursiveIterator(true);
+      Iterator<TreeNodeFace> it = ((TreeNodeResource)rootNode)._getNodes().recursiveIterator(true);
       while(it.hasNext())
       {
-         TreeNodeResource<N> node = it.next();
+         final TreeNodeFace node = it.next();
          if (found)
          {
-            return node.getNodeId();
+            return ((TreeNodeResource)node).getNodeId();
          }
 
-         found = rootNode._getNodeId() == nodeId.intValue();
+         found = ((TreeNodeResource)rootNode)._getNodeId() == nodeId.intValue();
       }
 
       return new integer();
@@ -707,7 +700,7 @@
    @Override
    public integer getNodeFgColor(NumberType nodeId)
    {
-      return doWithNode(nodeId, null, new integer(), n -> new integer(n.getFgColor()));
+      return doWithNode(nodeId, null, new integer(), n -> new integer(((TreeNodeResource)n).getFgColor()));
    }
 
    /**
@@ -724,7 +717,7 @@
    {
       doWithNode(nodeId, new BaseDataType[] {fgrValue}, n ->
       {
-         n.setFgColor(fgrValue.intValue());
+         ((TreeNodeResource)n).setFgColor(fgrValue.intValue());
          pushNodes();
       });
    }
@@ -742,7 +735,7 @@
    @Override
    public integer getNodeBgColor(NumberType nodeId)
    {
-      return doWithNode(nodeId, null, new integer(), n -> new integer(n.getFgColor()));
+      return doWithNode(nodeId, null, new integer(), n -> new integer(((TreeNodeResource)n).getFgColor()));
    }
 
    /**
@@ -758,7 +751,7 @@
    {
       doWithNode(nodeId, new BaseDataType[] {bgrValue}, n ->
       {
-         n.setBgColor(bgrValue.intValue());
+         ((TreeNodeResource)n).setBgColor(bgrValue.intValue());
          pushNodes();
       });
    }
@@ -889,37 +882,37 @@
       }
 
       final int id = parentNodeId.intValue();
-      final TreeNodeResource<N> parent = id == 0 ? rootNode : findNode(parentNodeId.intValue());
+      final  TreeNodeFace parent = id == 0 ? rootNode : findNode(parentNodeId.intValue());
       if (parent == null)
       {
          return;
       }
 
-      final handle hnode = parent._getNodes()._addNodeImpl(null, TreeNodeCollectionResource.Location.LAST,
+      final handle hnode = ((TreeNodeResource)parent)._getNodes()._addNodeImpl(null, TreeNodeCollectionResource.Location.LAST,
                                                      nodeKey, nodeText, false);
       if (hnode == null || hnode.isUnknown())
       {
          return;
       }
 
-      TreeNodeResource<N> node = (TreeNodeResource<N>) hnode.getResource();
-      node.setNodeIcon(collapseIcon);
-      node.setExpandNodeIcon(expandIcon);
+      final  TreeNodeFace node = (TreeNodeResource) hnode.getResource();
+      ((TreeNodeResource)node).setNodeIcon(collapseIcon);
+      ((TreeNodeResource)node).setExpandNodeIcon(expandIcon);
       if (hasChildren != null && !hasChildren.isUnknown())
       {
-         node._setHasChildren(hasChildren.booleanValue());
+         ((TreeNodeResource)node)._setHasChildren(hasChildren.booleanValue());
       }
 
       if (tooltip != null && !tooltip.isUnknown())
       {
-         node._setTooltip(tooltip.getValue());
+         ((TreeNodeResource)node)._setTooltip(tooltip.getValue());
       }
 
       pushNodes();
 
       if (BaseDataType.isAllKnown(newNodeId))
       {
-         newNodeId.assign(node.getNodeId());
+         newNodeId.assign(((TreeNodeResource)node).getNodeId());
       }
    }
 
@@ -962,17 +955,18 @@
    public void removeNode(NumberType nodeId)
    {
       doWithNode(nodeId, null, n -> {
-         final TreeNodeResource<N> parent = n._getParent();
+         final TreeNodeFace parent = ((TreeNodeResource)n)._getParent();
          // We are not going to delete the tree root!
          assert parent != null;
 
-         final TreeNodeCollectionResource<N, C, R> children = parent._getNodes();
+         final TreeNodeCollectionResource children = ((TreeNodeResource)parent)._getNodes();
 
          /**
           * If the selected node is being deleted or the selected node descends from
           * the node being deleted, adjust the selection.
           */
-         for (R node = _getSelectedNode(); node != rootNode; node = (R) node._getParent())
+         for (TreeNodeFace node = _getSelectedNode(); node != rootNode; node = ((TreeNodeResource) node)
+                  ._getParent())
          {
             if (node == n)
             {
@@ -985,7 +979,7 @@
                final int nodeChildIndex = children._indexOf(n);
                assert nodeChildIndex >= 0;
 
-               final TreeNodeResource<N> newFocused;
+               final  TreeNodeFace newFocused;
                if (nodeChildIndex > 0)
                {
                   newFocused = children._getNode(nodeChildIndex - 1);
@@ -1003,14 +997,14 @@
                   newFocused = rootNode;
                }
 
-               _setSelectedNode(newFocused._getNodeId());
+               _setSelectedNode(((TreeNodeResource)newFocused)._getNodeId());
 
                break;
             }
          }
          
          children._removeNode(n, null);
-         parent._setHasChildren(children._getCount() != 0);
+         ((TreeNodeResource)parent)._setHasChildren(children._getCount() != 0);
 
          pushNodes();
       });
@@ -1027,7 +1021,7 @@
    @Override
    public logical isNodeExpanded(NumberType nodeId)
    {
-      return doWithNode(nodeId, null, new logical(), n -> n.isNodeExpanded());
+      return doWithNode(nodeId, null, new logical(), n -> ((TreeNodeResource)n).isNodeExpanded());
    }
 
    /**
@@ -1039,7 +1033,7 @@
    @Override
    public void expandNode(NumberType nodeId)
    {
-      doWithNode(nodeId, null, n -> n.setNodeExpanded(new logical(true)));
+      doWithNode(nodeId, null, n -> ((TreeNodeResource)n).setNodeExpanded(new logical(true)));
    }
 
    /**
@@ -1053,7 +1047,7 @@
    {
       doWithNode(nodeId, null, n -> 
       {
-         n.setNodeExpanded(new logical(false));
+         ((TreeNodeResource)n).setNodeExpanded(new logical(false));
          doSelectAfterCollapse(n);
       });
    }
@@ -1064,7 +1058,7 @@
    @Override
    public void clearAll()
    {
-      rootNode._removeCollection();
+      ((TreeNodeResource)rootNode)._removeCollection();
    }
 
    /**
@@ -1170,13 +1164,12 @@
    {
       doWithNode(nodeId, new BaseDataType[] {outChildId, outChildKey}, n ->
       {
-         TreeNodeCollectionResource<N,C,R> col = n._getNodes();
+         final TreeNodeCollectionResource col = ((TreeNodeResource)n)._getNodes();
          if (col._getCount() > 0)
          {
-            TreeNodeResource<N> result = null;
-            result = col._getNode(0);
-            outChildId.assign(result._getNodeId());
-            outChildKey.assign(result._getNodeKey());
+            final TreeNodeFace result = col._getNode(0);
+            outChildId.assign(((TreeNodeResource)result)._getNodeId());
+            outChildKey.assign(((TreeNodeResource)result)._getNodeKey());
          }
       });
    }
@@ -1196,16 +1189,16 @@
    {
       doWithNode(nodeId, new BaseDataType[] {outSiblingId, outSiblingKey}, n ->
       {
-         TreeNodeResource<N> parent = n._getParent();
+          TreeNodeFace parent = ((TreeNodeResource)n)._getParent();
          if (parent != null)
          {
-            TreeNodeCollectionResource<N,C,R> col = parent._getNodes();
+            TreeNodeCollectionResource col = ((TreeNodeResource)parent)._getNodes();
             int i = col._indexOf(n);
             if (i >= 0 && i < col._getCount() - 1)
             {
-               TreeNodeResource<N> result = col._getNode(i + 1);
-               outSiblingId.assign(result._getNodeId());
-               outSiblingKey.assign(result._getNodeKey());
+               final TreeNodeFace result = col._getNode(i + 1);
+               outSiblingId.assign(((TreeNodeResource)result)._getNodeId());
+               outSiblingKey.assign(((TreeNodeResource)result)._getNodeKey());
             }
             else
             {
@@ -1230,16 +1223,16 @@
    {
       doWithNode(nodeId, new BaseDataType[] {outSiblingId, outSiblingKey}, n ->
       {
-         TreeNodeResource<N> parent = n._getParent();
+          TreeNodeFace parent = ((TreeNodeResource)n)._getParent();
          if (parent != null)
          {
-            TreeNodeCollectionResource<N,C,R> col = parent._getNodes();
+            TreeNodeCollectionResource col = ((TreeNodeResource)parent)._getNodes();
             int i = col._indexOf(n);
             if (i >= 1 && i < col._getCount())
             {
-               TreeNodeResource<N> result = col._getNode(i - 1);
-               outSiblingId.assign(result._getNodeId());
-               outSiblingKey.assign(result._getNodeKey());
+                TreeNodeFace result = col._getNode(i - 1);
+               outSiblingId.assign(((TreeNodeResource)result)._getNodeId());
+               outSiblingKey.assign(((TreeNodeResource)result)._getNodeKey());
             }
             else
             {
@@ -1264,11 +1257,11 @@
    {
       doWithNode(nodeId, new BaseDataType[] {outParentId, outParentKey}, n ->
       {
-         TreeNodeResource<N> parent = n._getParent();
+          TreeNodeFace parent = ((TreeNodeResource)n)._getParent();
          if (parent != null)
          {
-            outParentId.assign(parent._getNodeId());
-            outParentKey.assign(parent._getNodeKey());
+            outParentId.assign(((TreeNodeResource)parent)._getNodeId());
+            outParentKey.assign(((TreeNodeResource)parent)._getNodeKey());
          }
          else
          {
@@ -1291,7 +1284,7 @@
    @Override
    public integer getNodeLevel(character nodeKey)
    {
-      return doWithNode(nodeKey, null, new integer(), n -> new integer(n._getNodeLevel()));
+      return doWithNode(nodeKey, null, new integer(), n -> new integer(((TreeNodeResource)n)._getNodeLevel()));
    }
 
    /**
@@ -1301,11 +1294,11 @@
     *          the node to make the top visible
     */
    @Override
-   public void scrollNodeToTop(character nodeKey)
+   public void scrollNodeToTop(final character nodeKey)
    {
       doWithNode(nodeKey, null, n ->
       {
-         setAttr("topNode", config.topNode, n._getNodeId(), (int vv) -> config.topNode = vv);
+         setAttr("topNode", config.topNode, ((TreeNodeResource)n)._getNodeId(), (int vv) -> config.topNode = vv);
       });
    }
 
@@ -1476,11 +1469,11 @@
                                                                                 y.intValue());
          if (nodeId != null)
          {
-            R node = findNode(nodeId.intValue());
+             TreeNodeFace node = findNode(nodeId.intValue());
             
             if (node != null)
             {
-               return node.getNodeKey();
+               return ((TreeNodeResource)node).getNodeKey();
             }
          }
       }
@@ -1521,8 +1514,8 @@
    {
       doWithNode(nodeId, null, n ->
       {
-         n.setNodeIcon(icon);
-         n.setExpandNodeIcon(expandIcon);
+         ((TreeNodeResource)n).setNodeIcon(icon);
+         ((TreeNodeResource)n).setExpandNodeIcon(expandIcon);
          pushNodes();
       });
    }
@@ -1599,7 +1592,7 @@
          return new handle(rootNode);
       }
 
-      R node = findNode(nodeId);
+       TreeNodeFace node = findNode(nodeId);
       return new handle(node);
    }
 
@@ -1662,8 +1655,8 @@
    @Override
    public void collapseAllExcept(handle node)
    {
-      TreeNodeResource<N> n = node == null || node.isUnknown() ?
-                           null : (TreeNodeResource<N>) node.getResource();
+       TreeNodeFace n = node == null || node.isUnknown() ?
+                           null : (TreeNodeResource) node.getResource();
       setAllNodesExpanded(false, n);
    }
 
@@ -1685,7 +1678,7 @@
 
       handle node = findNode(nodeId);
       return new logical(!node.isUnknown() && 
-            ((TreeNodeResource<N>) node.getResource())._getHasChildren());
+            ((TreeNodeResource) node.getResource())._getHasChildren());
    }
 
    /**
@@ -1707,7 +1700,7 @@
       handle node = findNode(nodeId);
       if (!node.isUnknown())
       {
-         ((TreeNodeResource<N>) node.getResource())._setHasChildren(value.booleanValue());
+         ((TreeNodeResource) node.getResource())._setHasChildren(value.booleanValue());
       }
    }
 
@@ -1746,7 +1739,7 @@
    @Override
    public integer getTopVisibleNodeId()
    {
-      return new integer(getAttr("topNode", () -> config.topNode, true));
+      return new integer(_getTopVisibleNodeId());
    }
 
    /**
@@ -1788,7 +1781,7 @@
    @Override
    public character getNodeText(character nodeKey)
    {
-      return doWithNode(nodeKey, null, new character(), node -> node.getNodeText());
+      return doWithNode(nodeKey, null, new character(), node -> ((TreeNodeResource)node).getNodeText());
    }
 
    /**
@@ -1802,7 +1795,7 @@
    @Override
    public void setNodeText(character nodeKey, character text)
    {
-      doWithNode(nodeKey, new BaseDataType[] {text}, node -> node.setNodeText(text));
+      doWithNode(nodeKey, new BaseDataType[] {text}, node -> ((TreeNodeResource)node).setNodeText(text));
    }
 
    /**
@@ -1813,7 +1806,7 @@
    {
       character nodeKey = getSelectedNodeKey();
       
-      doWithNode(nodeKey, null, node -> node.setEditorMode(EditorMode.ACTIVATING));
+      doWithNode(nodeKey, null, node -> ((TreeNodeResource)node).setEditorMode(EditorMode.ACTIVATING));
    }
 
    /**
@@ -1874,11 +1867,12 @@
          return new integer();
       }
 
-      R node = findNode(nodeKey.toStringMessage());
+      final TreeNodeFace node = findNode(nodeKey.toStringMessage());
       if (node != null)
       {
-         return new integer(node.nodeId);
+         return new integer(((TreeNodeResource)node).nodeId);
       }
+      
       return new integer();
    }
 
@@ -2043,7 +2037,7 @@
       
       super.delete();
       
-      rootNode.delete();
+      ((TreeNodeResource)rootNode).delete();
    }
 
    /**
@@ -2109,7 +2103,7 @@
          return new handle();
       }
       
-      R node = getNodeByCoordinates(x.intValue(), y.intValue());
+       TreeNodeFace node = getNodeByCoordinates(x.intValue(), y.intValue());
       
       return node == null ? new handle() : new handle(node);
    }
@@ -2133,11 +2127,11 @@
          return new comhandle();
       }
       
-      R node = getNodeByCoordinates(x.intValue(), y.intValue());
+      final  TreeNodeFace node = getNodeByCoordinates(x.intValue(), y.intValue());
       
-      if (node instanceof TreeViewNodeResource)
+      if (node instanceof TreeNodeResource)
       {
-         return TreeViewNodeObject.newInstance((TreeViewNodeResource) node);
+         return TreeViewNodeObject.newInstance((TreeNodeResource) node);
       }
       return new comhandle();
    }
@@ -2184,15 +2178,15 @@
     * @param   pushNodes
     *          When set to {@code true} the new node being added will be pushed to the client.
     */
-   protected void addNode(R node, boolean pushNodes)
+   protected void addNode(final TreeNodeFace node, boolean pushNodes)
    {
-      final integer nodeId = node.getNodeId();
+      final integer nodeId = ((TreeNodeResource)node).getNodeId();
       if (nodeId.isUnknown())
       {
          throw new RuntimeException("Node id value must not be unknown!");
       }
 
-      final character nodeKey = node.getNodeKey();
+      final character nodeKey = ((TreeNodeResource)node).getNodeKey();
       if (nodeKey.isUnknown())
       {
          throw new RuntimeException("Node key value must not be unknown!");
@@ -2214,9 +2208,9 @@
       nodesByIndices.add(node);
 
       // this is the first node added to the tree-list; select it
-      if (node._getParent() == rootNode && rootNode._getNodes()._getCount() == 1)
+      if (((TreeNodeResource)node)._getParent() == rootNode && ((TreeNodeResource)rootNode)._getNodes()._getCount() == 1)
       {
-         _setSelectedNode(node._getNodeId());
+         _setSelectedNode(((TreeNodeResource)node)._getNodeId());
       }
       
       if (pushNodes)
@@ -2237,7 +2231,7 @@
     *
     * @return  new node instance.
     */
-   abstract protected R createNode(String key, String text, R parent);
+   abstract protected TreeNodeFace createNode(String key, String text, TreeNodeFace parent);
 
    /**
     * Allocates and returns next node id.
@@ -2254,7 +2248,7 @@
     *
     * @return  a tree root node instance.
     */
-   protected R getRootNode()
+   protected TreeNodeFace getRootNode()
    {
       return rootNode;
    }
@@ -2270,16 +2264,16 @@
          return;
       }
 
-      ArrayList<N> result = new ArrayList<>();
+      final List<TreeNodeEntry> result = new ArrayList<>();
       
-      TreeNodeCollectionResource<N,C,R> coll = rootNode._getNodes(false);
+      final TreeNodeCollectionResource coll = ((TreeNodeResource)rootNode)._getNodes(false);
       if (coll != null)
       {
-         Iterator<R> it = coll.recursiveIterator(false);
+         final Iterator <TreeNodeFace >  it = coll.recursiveIterator(false);
          while (it.hasNext())
          {
-            TreeNodeResource<N> node = it.next();
-            result.add((N) node.saveToEntry());
+            final  TreeNodeFace node = it.next();
+            result.add(((TreeNodeResource)node).saveToEntry());
          }
 
          setAttr("nodes", config.nodes, result, (vv) -> config.nodes = vv);
@@ -2319,10 +2313,10 @@
     * @param   pushNodes
     *          When set to {@code true} the model change will be pushed to the client.
     */
-   void removeNode(R node, boolean pushNodes)
+   void removeNode(final TreeNodeFace node, boolean pushNodes)
    {
-      nodesById.remove(node.getNodeId().intValue());
-      nodesByKey.remove(node.getNodeKey().toStringMessage());
+      nodesById.remove(((TreeNodeResource)node).getNodeId().intValue());
+      nodesByKey.remove(((TreeNodeResource)node).getNodeKey().toStringMessage());
       // The node is unique so its first occurrence can be removed from this collection
       nodesByIndices.remove(node);
 
@@ -2342,10 +2336,10 @@
     * @param    pushNodes
     *           When set to {@code true} the model change will be pushed to the client.
     */
-   void updateNode(R node, String oldKeyValue, boolean pushNodes)
+   void updateNode(final TreeNodeFace node, String oldKeyValue, boolean pushNodes)
    {
       nodesByKey.remove(oldKeyValue);
-      nodesByKey.put(node.getNodeKey().toStringMessage(), node);
+      nodesByKey.put(((TreeNodeResource)node).getNodeKey().toStringMessage(), node);
 
       if (pushNodes)
       {
@@ -2361,7 +2355,7 @@
     *
     * @return  a tree node instance or null if not found.
     */
-   R findNode(String key)
+    TreeNodeFace findNode(String key)
    {
       return nodesByKey.get(key);
    }
@@ -2374,7 +2368,7 @@
     *
     * @return  a tree node instance or null if not found.
     */
-   R findNode(int nodeId)
+   TreeNodeFace findNode(int nodeId)
    {
       return nodesById.get(nodeId);
    }
@@ -2390,7 +2384,7 @@
     *
     * @return   The tree node instance or null if an index is out of bounds.
     */
-   R getTreeNodeByIndex(int index)
+   TreeNodeFace getTreeNodeByIndex(int index)
    {
       if (index < 0 || index >= nodesByIndices.size())
       {
@@ -2408,7 +2402,7 @@
     *
     * @return   The 0-based index of the the target node.
     */
-   int getTreeNodeIndex(R node)
+   int getTreeNodeIndex(final TreeNodeFace node)
    {
       return nodesByIndices.indexOf(node);
    }
@@ -2458,7 +2452,7 @@
    protected <T extends BaseDataType> T doWithNode(NumberType nodeId,
                                                    BaseDataType[] mandatoryArgs,
                                                    T failResult,
-                                                   Function<R, T> body)
+                                                   Function<TreeNodeFace, T> body)
    {
       return doWithNodeImpl(nodeId,
                             mandatoryArgs,
@@ -2482,7 +2476,7 @@
     */
    protected void doWithNode(NumberType nodeId,
                              BaseDataType[] mandatoryArgs,
-                             Consumer<R> body)
+                             Consumer <TreeNodeFace>  body)
    {
       doWithNode(nodeId, mandatoryArgs, null, n ->
       {
@@ -2514,7 +2508,7 @@
    protected <T extends BaseDataType> T doWithNode(character nodeKey,
                                                    BaseDataType[] mandatoryArgs,
                                                    T failResult,
-                                                   Function<R, T> body)
+                                                   Function<TreeNodeFace, T> body)
    {
       return doWithNodeImpl(nodeKey,
                             mandatoryArgs,
@@ -2538,7 +2532,7 @@
     */
    protected void doWithNode(character nodeKey,
                              BaseDataType[] mandatoryArgs,
-                             Consumer<TreeNodeResource<N>> body)
+                             Consumer<TreeNodeFace> body)
    {
       doWithNode(nodeKey, mandatoryArgs, null, n ->
       {
@@ -2569,11 +2563,11 @@
     *
     * @return  see above
     */
-   protected <T extends BaseDataType> T doWithNodeImpl(BaseDataType nodeId,
+   protected static <T extends BaseDataType> T doWithNodeImpl(BaseDataType nodeId,
                                                        BaseDataType[] mandatoryArgs,
                                                        T failResult,
-                                                       Function<R, T> body,
-                                                       Function<BaseDataType, R> findNode)
+                                                       Function<TreeNodeFace, T> body,
+                                                       Function<BaseDataType, TreeNodeFace> findNode)
    {
       if (mandatoryArgs != null)
       {
@@ -2591,7 +2585,7 @@
          return failResult;
       }
 
-      R node = findNode.apply(nodeId);
+       TreeNodeFace node = findNode.apply(nodeId);
       if (node != null)
       {
          return body.apply(node);
@@ -2609,7 +2603,7 @@
     *
     * @return  see above.
     */
-   protected boolean assertKnown(BaseDataType... values)
+   protected static boolean assertKnown(BaseDataType... values)
    {
       for (BaseDataType val : values)
       {
@@ -2627,16 +2621,16 @@
     */
    void clearCollapsed()
    {
-      Iterator<TreeNodeResource<N>> it = rootNode._getNodes().recursiveIterator(true);
+      final Iterator<TreeNodeFace> it = ((TreeNodeResource)rootNode)._getNodes().recursiveIterator(true);
       while(it.hasNext())
       {
-         TreeNodeResource<N> node = it.next();
-         if (node._isNodeExpanded())
+          TreeNodeFace node = it.next();
+         if (((TreeNodeResource)node)._isNodeExpanded())
          {
             continue;
          }
 
-         TreeNodeCollectionResource<N, C, R> sub = node._getNodes(false);
+         TreeNodeCollectionResource sub = ((TreeNodeResource)node)._getNodes(false);
          if (sub != null && sub._getCount() > 0)
          {
             sub.clearNodes();
@@ -2652,29 +2646,29 @@
     * @param   exceptNode
     *          The node to exclude from collapsing/expanding, if null all nodes will be collapsed/expanded.
     */
-   private void setAllNodesExpanded(boolean expanded, TreeNodeResource<N> exceptNode)
+   private void setAllNodesExpanded(boolean expanded,  TreeNodeFace exceptNode)
    {
-      TreeNodeCollectionResource<N,C,R> nodes = rootNode._getNodes(false);
+      final TreeNodeCollectionResource nodes = ((TreeNodeResource)rootNode)._getNodes(false);
       if (nodes != null)
       {
-         Iterator<R> it = nodes.recursiveIterator(false);
+         Iterator <TreeNodeFace >  it = nodes.recursiveIterator(false);
          while(it.hasNext())
          {
-            TreeNodeResource<N> node = it.next();
-            if (node._isNodeExpanded() == expanded)
+             TreeNodeFace node = it.next();
+            if (((TreeNodeResource)node)._isNodeExpanded() == expanded)
             {
                continue;
             }
 
-            TreeNodeResource<N> check = exceptNode;
+             TreeNodeFace check = exceptNode;
             while(check != null)
             {
-               if (check._getNodeId() == node._getNodeId())
+               if (((TreeNodeResource)check)._getNodeId() == ((TreeNodeResource)node)._getNodeId())
                {
                   break;
                }
 
-               check = check._getParent();
+               check = ((TreeNodeResource)check)._getParent();
             }
 
             if (check != null)
@@ -2682,7 +2676,7 @@
                continue;
             }
 
-            node._setNodeExpanded(expanded);
+            ((TreeNodeResource)node)._setNodeExpanded(expanded);
          }
 
          pushNodes();
@@ -2694,7 +2688,7 @@
     * 
     * @return the selected node
     */
-   private final R _getSelectedNode()
+   private final  TreeNodeFace _getSelectedNode()
    {
       return nodesById.get(_getSelecteNodeId());
    }
@@ -2719,7 +2713,7 @@
     * 
     * @return   The tree node if there exists one, otherwise null.
     */
-   private R getNodeByCoordinates(int x, int y)
+   private  TreeNodeFace getNodeByCoordinates(int x, int y)
    {
       Integer nodeId = null;
       
@@ -2734,7 +2728,7 @@
          return null;
       }
       
-      R node = this.findNode(nodeId);
+      TreeNodeFace node = this.findNode(nodeId);
       
       return node;
    }
@@ -2747,13 +2741,13 @@
     * 
     * @return   The node id if the provided value represents a valid node, otherwise Integer.MIN_VALUE.
     */
-   private int getNodeIdFrom(handle value)
+   private static int getNodeIdFrom(final handle value)
    {
       int nodeId = Integer.MIN_VALUE;
       
       if (value != null && !value.isUnknown())
       {
-         nodeId = ((TreeNodeResource<N>) value.getResource()).getNodeId().intValue();
+         nodeId = ((TreeNodeResource) value.getResource()).getNodeId().intValue();
       }
       
       return nodeId;
@@ -2767,9 +2761,9 @@
     * @param   collapsedNode
     *          The node on which the collapse operation was done.
     */
-   private void doSelectAfterCollapse(TreeNodeResource<N> collapsedNode)
+   private void doSelectAfterCollapse( TreeNodeFace collapsedNode)
    {
-      TreeNodeResource<N> selectedAncestor = ((TreeNodeResource<N>) getSelectedNode().getResource());
+       TreeNodeFace selectedAncestor = ((TreeNodeResource) getSelectedNode().getResource());
       
       if (!validSelection() || collapsedNode == selectedAncestor)
       {
@@ -2780,17 +2774,27 @@
              selectedAncestor != collapsedNode && 
              selectedAncestor != rootNode)
       {
-         selectedAncestor = selectedAncestor._getParent();
+         selectedAncestor = ((TreeNodeResource)selectedAncestor)._getParent();
       } 
       
       
       if (selectedAncestor == collapsedNode)
       {
-         _setSelectedNode(collapsedNode._getNodeId());
+         _setSelectedNode(((TreeNodeResource)collapsedNode)._getNodeId());
       }
    }
    
    /**
+    * Returns the ID of the top visible node or -1 if this tree is empty.
+    *
+    * @return see above.
+    */
+   private final int _getTopVisibleNodeId()
+   {
+      return getAttr("topNode", () -> config.topNode, true);
+   }
+
+   /**
     * Check if the current selected node is valid: is known and attached to this tree.
     * 
     * @return   {@true} if the selected node is known and belongs to this tree.

=== modified file 'src/com/goldencode/p2j/ui/client/gui/TreeBodyGuiImpl.java'
--- src/com/goldencode/p2j/ui/client/gui/TreeBodyGuiImpl.java	2021-06-10 10:44:35 +0000
+++ src/com/goldencode/p2j/ui/client/gui/TreeBodyGuiImpl.java	2021-06-18 19:29:15 +0000
@@ -120,6 +120,7 @@
 import com.goldencode.p2j.ui.client.gui.TreeGuiImpl.*;
 import com.goldencode.p2j.ui.client.gui.driver.*;
 import com.goldencode.p2j.ui.client.gui.theme.*;
+import com.goldencode.p2j.ui.client.model.*;
 import com.goldencode.p2j.ui.client.widget.*;
 
 // this usage of AWT events is intentional, they are being used separately from the
@@ -139,18 +140,12 @@
  * @param <N> 
  *        node type
  */
-public abstract class TreeBodyGuiImpl<
-      C extends TreeConfig<N>,
-      T extends TreeGuiImpl<C, ? extends TreeBodyGuiImpl<C,T,N>, N>,
-      N extends TreeNodeEntry>
+public abstract class TreeBodyGuiImpl
 extends AbstractWidget<GuiOutputManager>
 implements ScrollableWidget<GuiOutputManager>,
-           ScrollPublisher,
-           TreeViewDelegate
+           TreeViewDelegate,
+           I2DScrollable
 {
-   /** Widget id */
-   private final WidgetId id;
-
    /** Screen driver */
    protected GuiDriver<?,?> gd;
 
@@ -170,22 +165,22 @@
    protected ColorRgb disabledFore;
 
    /** The owning tree view */
-   protected T tree;
+   protected TreeGuiImpl tree;
 
    /** The theme used for drawing */
    protected ClassicTheme theme;
 
-   /** Calculated widget width */
-   private double width;
-
-   /** Calculated widget height */
-   private double height;
+   /**
+    * The widget config. The application sets the {@link BaseConfig#widthChars}
+    * and {@link BaseConfig#heightChars} while laying out.
+    */
+   protected BaseConfig config = new BaseConfig();
 
    /** List of scroll listeners */
    private List<ScrollListener> scrollListeners = new LinkedList<>();
 
    /** The node with active tooltip */
-   private TreeGuiImpl.TreeNode<N> tooltipNode;
+   private TreeNode tooltipNode;
 
    /**
     * Constructor.
@@ -193,13 +188,16 @@
     * @param   tree
     *          The owning tree view.
     */
-   public TreeBodyGuiImpl(T tree)
+   public TreeBodyGuiImpl(final TreeGuiImpl tree)
    {
-      id = WidgetId.nextID();
+      config.id = WidgetId.nextID();
       
       this.tree = tree;
       
-      TreeConfig<N> c = tree.config();
+      tree.hModel.addListener(this);
+      tree.vModel.addListener(this);
+
+      final TreeConfig c = tree.config();
 
       // initialize screen driver
       gd = (GuiDriver<?,?>) OutputManager.getDriver();
@@ -211,7 +209,7 @@
       updateColors(c);
       disabledFore  = gd.getSysColor(SysColor.COLOR_3DSHADOW);
 
-      int bgColor = isThreeD() && !c.enabled ? SysColor.COLOR_3DFACE : SysColor.COLOR_WINDOW;
+      final int bgColor = isThreeD() && !c.enabled ? SysColor.COLOR_3DFACE : SysColor.COLOR_WINDOW;
       gc = new GuiColorResolver(gd, c, bgColor, SysColor.COLOR_BTNTEXT);
 
       // at this point classic theme is expected
@@ -224,7 +222,7 @@
     * @param   c
     *          Treeview config.
     */
-   public void updateColors(TreeConfig<N> c)
+   public void updateColors(TreeConfig c)
    {
       ColorRgb colorRgb = new ColorRgb();
       colorRgb.setRgb(c.getSelectedNodeBg());
@@ -245,7 +243,7 @@
    @Override
    public WidgetId getId()
    {
-      return id;
+      return config.id;
    }
 
    /**
@@ -282,14 +280,14 @@
       }
 
       final int key = event.keyCode();
-      final TreeGuiImpl.TreeNode<N> currentNode = tree.currentNode;
+      final TreeNode currentNode = tree.currentNode;
       final TreeNodeEntry nodeValue = currentNode == null ? null : currentNode.value;
       switch (key)
       {
          case Key.VK_HOME:
          {
             // Navigate to the first visible node
-            final TreeNode<N> firstVisible = firstVisible();
+            final TreeNode firstVisible = firstVisible();
             if (firstVisible != null)
             {
                tree.setCurrentNode(firstVisible);
@@ -300,10 +298,10 @@
          case Key.VK_END:
          {
             // Navigate to the last visible node
-            final TreeNode<N> firstVisible = firstVisible();
+            final TreeNode firstVisible = firstVisible();
             if (firstVisible != null)
             {
-               final TreeGuiImpl.TreeNode<N> lastVisible = getNextVisibleNode(firstVisible, false,
+               final TreeNode lastVisible = getNextVisibleNode(firstVisible, false,
                         0, true);
                tree.setCurrentNode(lastVisible, true, true);
             }
@@ -399,7 +397,7 @@
       deactivateTooltip();
       
       final NativePoint n = translate(e);
-      final TreeGuiImpl.TreeNode<N> node = isNodeContentAtLocation(n);
+      final TreeNode node = isNodeContentAtLocation(n);
       if (node != null)
       {
          if (node != tree.getCurrentNode())
@@ -417,7 +415,7 @@
          return;
       }
       
-      final TreeGuiImpl.TreeNode<N> toggleNode = isExpandIconAtLocation(n);
+      final TreeNode toggleNode = isExpandIconAtLocation(n);
       if (toggleNode != null)
       {
          tree.toggleNodeExpansion(toggleNode);
@@ -433,7 +431,7 @@
    @Override
    public void mouseClicked(final MouseEvent e)
    {
-      final TreeGuiImpl.TreeNode<N> node = isNodeContentAtLocation(translate(e));
+      final TreeNode node = isNodeContentAtLocation(translate(e));
 
       if (node != null)
       {
@@ -441,7 +439,7 @@
       }
       
       final int clickCount = e.getClickCount();
-      final C config = tree.config();
+      final TreeConfig config = tree.config();
       if (node != null
           && ((clickCount == 1 && config.expandOnSingleClick) || 
               (clickCount == 2 && config.expandOnDoubleClick)))
@@ -480,10 +478,18 @@
     *          The mouse event.
     */
    @Override
-   public void mouseWheelMoved(MouseWheelEvent e)
+   public void mouseWheelMoved(final MouseWheelEvent e)
    {
       tree.getTreeEditor().deactivateEditor();
-      tree.scrollpane.mouseWheelMoved(e);
+      final int step = e.getWheelRotation();
+      if (e.isControlDown())
+      {
+         tree.hModel.increment(step);
+      }
+      else
+      {
+         tree.vModel.increment(step);
+      }
    }
 
    /**
@@ -493,10 +499,10 @@
     *          The mouse event.
     */
    @Override
-   public void mouseMoved(MouseEvent e)
+   public void mouseMoved(final MouseEvent e)
    {
       NativePoint n = translate(e);
-      TreeGuiImpl.TreeNode<N> node = isNodeContentAtLocation(n);
+      final TreeNode node = isNodeContentAtLocation(n);
       if (node != tooltipNode && tooltipNode != null)
       {
          deactivateTooltip();
@@ -504,7 +510,16 @@
 
       if (node != null && node != tooltipNode)
       {
-         activateTooltip(node, new NativePoint(e.getX(), e.getY()));
+         // This holds for any visible node:
+         assert node.value != null;
+         
+         final String tooltip = node.value.tooltip;
+         if (tooltip != null && !tooltip.isEmpty())
+         {
+            final ToolTip tt = ToolTip.getInstance();
+            tt.activate(this, tooltip, -1, new NativePoint(e.getX(), e.getY()));
+            tooltipNode = node;
+         }
       }
    }
 
@@ -564,7 +579,7 @@
    @Override
    public double width()
    {
-      return width;
+      return config.widthChars;
    }
 
    /**
@@ -575,7 +590,7 @@
    @Override
    public double height()
    {
-      return height;
+      return config.heightChars;
    }
 
    /**
@@ -590,18 +605,6 @@
    }
 
    /**
-    * Add scroll listener.
-    *
-    * @param   listener
-    *          Listener to add.
-    */
-   @Override
-   public void addScrollListener(ScrollListener listener)
-   {
-      scrollListeners.add(listener);
-   }
-
-   /**
     * Returns the scroll dimension of this widget. The scroll dimension is
     * the full size of the widget's content, it may be bigger than widget's
     * visible size.
@@ -662,27 +665,29 @@
     *          New Y scroll position.
     */
    @Override
-   public void scroll(Integer newXPosition, Integer newYPosition)
+   public void scroll(final Integer newXPosition, final Integer newYPosition)
    {
-      if (newXPosition == null)
-      {
-         newXPosition = physicalLocation().x;
-      }
-      else
-      {
-         newXPosition = -newXPosition;
-      }
-
-      if (newYPosition == null)
-      {
-         newYPosition = physicalLocation().y;
-      }
-      else
-      {
-         newYPosition = -newYPosition;
-      }
-
-      setPhysicalLocation(newXPosition, newYPosition);
+      Integer x = newXPosition;
+      Integer y = newYPosition;
+      if (x == null)
+      {
+         x = physicalLocation().x;
+      }
+      else
+      {
+         x = -x;
+      }
+
+      if (y == null)
+      {
+         y = physicalLocation().y;
+      }
+      else
+      {
+         y = -y;
+      }
+
+      setPhysicalLocation(x, y);
       
       int oldTopNode = tree.config().topNode;
       int newTopNode = getTopNode().value.nodeId;
@@ -724,8 +729,9 @@
    public Integer hitTest(int x, int y)
    {
       NativePoint point = getLocationRelativeToBody(x, y);
+      final NativePoint point1 = point;
       
-      TreeGuiImpl.TreeNode<N> node = hitTest(point);
+      TreeNode node = isNodeContentAtLocation(point1);
       
       return node != null && node.value != null ? node.value.nodeId : null;
    }
@@ -744,42 +750,48 @@
    public abstract NativePoint getLocationRelativeToBody(int x, int y);
    
    /**
-    * Calculates the widget size.
+    * Calculates the widget size:
+    * set config.widthChars, config.heightChars, tree.config().topNode
+    * 
+    * Called from {@link TreeGuiImpl#buildTreeModel()}, and {@link TreeGuiImpl#toggleNodeExpansion(TreeNode)}
     */
-   public void calcSize()
+   public final void calcSize()
    {
-      ScrollPane<?> sp = parent(ScrollPane.class);
-      Insets spInsets = sp.insets();
-      Dimension spDim = sp.dimension();
-      double spWidth = Coordinate.scale(spDim.width - spInsets.widthDelta());
-      double spHeight = Coordinate.scale(spDim.height - spInsets.heightDelta());
+      final Dimension spDim = parent().dimension();
+      final double spWidth = spDim.width;
+      final double spHeight = spDim.height;
 
-      TreeGuiImpl.TreeNode<N>  rootNode = tree.rootNode;
+      final TreeNode rootNode = tree.rootNode;
       if (rootNode == null || rootNode.nodes == null)
       {
-         width = spWidth;
-         height = spHeight;
+         config.widthChars = spWidth;
+         config.heightChars = spHeight;
          return;
       }
 
       // count visible nodes count
-      int nodeCount = 0;
-      TreeGuiImpl.NodesIterator<N> it = new TreeGuiImpl.NodesIterator<>(rootNode, true);
-      while(it.hasNext())
-      {
-         nodeCount++;
-         it.next();
-      }
-
-      int scrollBarWidth = ThemeManager.getCurrentTheme().getScrollBarButtonWidth();
-      int nativeWidth = calcMaxNodesWidth(rootNode.nodes, 0, 0) + 2 * TREEVIEW_INNER_PADDING + scrollBarWidth;
-      int nativeHeight = getNodeHeight() * nodeCount;
-
-      CoordinatesConversion cc = screen().coordinates();
-      width = Math.max(cc.widthFromNative(nativeWidth), spWidth);
-      height = Math.max(cc.heightFromNative(nativeHeight), spHeight);
-      final N value = getTopNode().value;
-      tree.config().topNode = value != null ? value.nodeId : 0;
+      final int nodeCount = getVisibleNodeCount();
+
+      final int scrollBarWidth = ThemeManager.getCurrentTheme().getScrollBarButtonWidth();
+      final int nativeWidth = calcMaxNodesWidth(rootNode.nodes, 0, 0) + 2 * TREEVIEW_INNER_PADDING
+               + scrollBarWidth;
+      final int nativeHeight = getNodeHeight() * nodeCount;
+
+      final CoordinatesConversion cc = screen().coordinates();
+      config.widthChars = Math.max(cc.widthFromNative(nativeWidth), spWidth);
+      config.heightChars = Math.max(cc.heightFromNative(nativeHeight), spHeight);
+      
+      final TreeNode topNode = getTopNode();      
+      final TreeNodeEntry value;
+      if (topNode != null)
+      {
+         value = topNode.value;
+      }
+      else
+      {
+         value = null;
+      }
+      tree.config().topNode = value != null ? value.nodeId : -1;
    }
    
    /**
@@ -790,7 +802,7 @@
     */
    void scrollToTop(int nodeId)
    {
-      TreeGuiImpl.TreeNode<N>  node = tree.findNode(nodeId);
+      TreeNode node = tree.findNode(nodeId);
       if (node != null)
       {
          ensureVisible(node, true, false);
@@ -832,21 +844,21 @@
     *
     * @return  array of size 2, at index 0 the found node, at index 1 the total node count.
     */
-   private TreeGuiImpl.TreeNode<N>  getNextVisibleNode(TreeGuiImpl.TreeNode<N>  relativeTo,
-                                       boolean forward,
-                                       int skip,
-                                       boolean rollover)
+   private TreeNode getNextVisibleNode(TreeNode relativeTo,
+                                       final boolean forward,
+                                       final int skip,
+                                       final boolean rollover)
    {
-      TreeGuiImpl.NodesIterator<N> it = new TreeGuiImpl.NodesIterator<>(tree.rootNode, true, !forward);
-      TreeGuiImpl.TreeNode<N>  result = null;
+      final NodesIterator it = new NodesIterator(tree.rootNode, true, !forward);
+      TreeNode  result = null;
       int count = 0;
       boolean relativeFound = relativeTo == null && forward;
-      TreeGuiImpl.TreeNode<N>  first = null;
-      TreeGuiImpl.TreeNode<N>  last = null;
+      TreeNode  first = null;
+      TreeNode  last = null;
 
       while (it.hasNext())
       {
-         TreeGuiImpl.TreeNode<N>  node = it.next();
+         final TreeNode node = it.next();
 
          if (first == null)
          {
@@ -885,9 +897,9 @@
     * 
     * @return the first visible node or {@code null} if tree has no visible nodes.
     */
-   private final TreeGuiImpl.TreeNode<N> firstVisible()
+   private final TreeNode firstVisible()
    {
-      final TreeGuiImpl.NodesIterator<N> it = new TreeGuiImpl.NodesIterator<>(tree.rootNode, true, false);
+      final NodesIterator it = new NodesIterator(tree.rootNode, true, false);
       return it.hasNext() ? it.next() : null;
    }
    
@@ -918,22 +930,22 @@
     *
     * @return  the found node or {@code null}.
     */
-   protected TreeGuiImpl.TreeNode<N>  isNodeContentAtLocation(NativePoint loc)
+   protected TreeNode isNodeContentAtLocation(final NativePoint loc)
    {
       if (loc.y < 0)
       {
          return null;
       }
 
-      int nodeHeight = getNodeHeight();
-      int row = loc.y / nodeHeight;
-      TreeGuiImpl.TreeNode<N>  node = getVisibleNode(row);
+      final int nodeHeight = getNodeHeight();
+      final int row = loc.y / nodeHeight;
+      final TreeNode  node = getVisibleNode(row);
       if (node == null)
       {
          return null;
       }
 
-      int level = tree.getNodeLevel(node);
+      int level = node.getLevel();
       
       int nodeTextOffset = TREEVIEW_INNER_PADDING + theme.getTreeViewNodeHorizontalOffset(level);
       
@@ -942,7 +954,7 @@
          return null;
       }
 
-      NativeRectangle labelBounds = tree.getNodeLabelBounds(node);
+      final NativeRectangle labelBounds = tree.getNodeLabelBounds(node);
       
       if (loc.x > labelBounds.right())
       {
@@ -960,16 +972,16 @@
     *
     * @return  the found node or {@code null}.
     */
-   private TreeGuiImpl.TreeNode<N>  isExpandIconAtLocation(NativePoint loc)
+   private TreeNode isExpandIconAtLocation(final NativePoint loc)
    {
       if (!tree.config().showExpandIcon)
       {
          return null;
       }
 
-      int nodeHeight = getNodeHeight();
-      int row = loc.y / nodeHeight;
-      TreeGuiImpl.TreeNode<N>  node = getVisibleNode(row);
+      final int nodeHeight = getNodeHeight();
+      final int row = loc.y / nodeHeight;
+      final TreeNode node = getVisibleNode(row);
       if (node == null)
       {
          return null;
@@ -980,8 +992,8 @@
          return null;
       }
 
-      int level = tree.getNodeLevel(node);
-      int iconX = TREEVIEW_INNER_PADDING +
+      final int level = node.getLevel();
+      final int iconX = TREEVIEW_INNER_PADDING +
       TREEVIEW_EXPAND_ICON_OFFSET + theme.getTreeViewNodeHorizontalOffset(level - 1);
       if (loc.x < iconX)
       {
@@ -1005,9 +1017,9 @@
     *
     * @return  the found node or {@code null}.
     */
-   protected TreeGuiImpl.TreeNode<N>  getVisibleNode(int rowIndex)
+   protected TreeNode getVisibleNode(final int rowIndex)
    {
-      TreeGuiImpl.NodesIterator<N> it = new TreeGuiImpl.NodesIterator<>(tree.rootNode);
+      final NodesIterator it = new NodesIterator(tree.rootNode);
       int i = 0;
       while (it.hasNext())
       {
@@ -1033,6 +1045,7 @@
     * 
     * @return see above
     */
+   @SuppressWarnings("static-method")
    protected boolean isVertThrough()
    {
       return false;
@@ -1050,22 +1063,22 @@
     *
     * @return  the calculated width.
     */
-   protected abstract int calcMaxNodesWidth(List<TreeGuiImpl.TreeNode<N> > nodes,
+   protected abstract int calcMaxNodesWidth(List<TreeNode > nodes,
                                             int maxWidth,
                                             int level);
 
    /**
     * Get the node instance which is on the top of the viewport.
     * 
-    * @return  The node on top of the viewport or the root node if no node in viewport.
+    * @return  The node on top of the viewport or the {@code null} if no node in viewport.
     * 
     */
-   private TreeNode<N> getTopNode()
+   private final TreeNode getTopNode()
    {
-      int idx = getTopNodeIndex();
-      TreeGuiImpl.NodesIterator<N> it = new TreeGuiImpl.NodesIterator<>(tree.rootNode);
+      final NodesIterator it = new NodesIterator(tree.rootNode);
       
-      while (it.hasNext() && idx > 0)
+      int idx = tree.vModel.getPosition();      
+      while (idx > 0 && it.hasNext())
       {
          it.next();
          idx--;
@@ -1073,7 +1086,7 @@
       
       if (!it.hasNext())
       {
-         return tree.rootNode;
+         return null;
       }
       
       return it.next();
@@ -1086,8 +1099,8 @@
     */
    private int getTopNodeIndex()
    {
-      int rowHeight = getNodeHeight();
-      NativePoint loc = physicalLocation();
+      final int rowHeight = getNodeHeight();
+      final NativePoint loc = physicalLocation();
       return Math.abs((int) Math.ceil((double) loc.y / rowHeight));
    }
 
@@ -1102,7 +1115,7 @@
     * @param   forceBottom
     *          Whether the node should be placed at the bottom of the view.
     */
-   void ensureVisible(TreeGuiImpl.TreeNode<N>  node, boolean forceTop, boolean forceBottom)
+   void ensureVisible(TreeNode  node, boolean forceTop, boolean forceBottom)
    {
       Integer scrollX = null;
       
@@ -1172,12 +1185,13 @@
       setMouseCursor(CursorType.DEFAULT);
 
       NativePoint point = translate(e);
-      TreeGuiImpl.TreeNode<N>  target = hitTest(point);
+      final NativePoint point1 = point;
+      TreeNode  target = isNodeContentAtLocation(point1);
       Object data = OleDragDrop.getData(CF_TEXT);
       
-      if (target != null && data instanceof TreeGuiImpl.TreeNode)
+      if (target != null && data instanceof TreeNode)
       {
-         TreeGuiImpl.TreeNode<N>  source = (TreeGuiImpl.TreeNode<N> ) data;
+         TreeNode  source = (TreeNode ) data;
 
          if (target.value.equals(source.value))
          {
@@ -1226,7 +1240,8 @@
       setMouseCursor(CursorType.TEXT);
 
       NativePoint point = translate(e);
-      TreeGuiImpl.TreeNode<N>  source = hitTest(point);
+      final NativePoint point1 = point;
+      TreeNode  source = isNodeContentAtLocation(point1);
       if (source != null)
       {
          if (source.parent == null)
@@ -1261,7 +1276,8 @@
    protected void oleDragging(MouseEvent e)
    {
       NativePoint point = translate(e);
-      TreeGuiImpl.TreeNode<N> overNode = hitTest(point);
+      final NativePoint point1 = point;
+      TreeNode overNode = isNodeContentAtLocation(point1);
       if (overNode != null)
       {
          if (overNode.parent == null)
@@ -1286,22 +1302,24 @@
     * @param   newYPosition
     *          New Y position.
     */
-   private void requestScroll(Integer newXPosition, Integer newYPosition)
+   private void requestScroll(final Integer newXPosition, final Integer newYPosition)
    {
+      Integer x = newXPosition;
+      Integer y = newYPosition;
       NativeDimension max = getScrollDimension();
-      if (newXPosition != null && newXPosition > max.width)
+      if (x != null && x > max.width)
       {
-         newXPosition = max.width;
+         x = max.width;
       }
 
-      if (newYPosition != null && newYPosition > max.height)
+      if (y != null && y > max.height)
       {
-         newYPosition = max.height;
+         y = max.height;
       }
 
       for (ScrollListener scrollListener : scrollListeners)
       {
-         scrollListener.handleScrollEvent(new ScrollEvent(this, newXPosition, newYPosition));
+         scrollListener.handleScrollEvent(new ScrollEvent(this, x, y));
       }
    }
 
@@ -1311,7 +1329,7 @@
     * @param   node
     *          The target node.
     */
-   private void triggerDefaultAction(TreeGuiImpl.TreeNode<N>  node)
+   private void triggerDefaultAction(TreeNode  node)
    {
       tree.config().triggerNode = node.value.nodeId;
       EventManager.postEvent(new KeyInput(tree, EventType.KEY_PRESSED, Keyboard.SE_DEFAULT_ACTION));
@@ -1328,7 +1346,7 @@
     */
    private void fireChangeTopVisibleNode(int prevNodeId, int scrollNodeCount)
    {
-      C config = tree.config();
+      final TreeConfig config = tree.config();
       config.triggerNode = prevNodeId;
       config.scrollNodeCount = scrollNodeCount;
       
@@ -1343,7 +1361,7 @@
     * 
     * @return   True if the trigger code returns NO-APPLY, otherwise false.
     */
-   private boolean fireNodeClickEvent(TreeGuiImpl.TreeNode<N>  node)
+   private boolean fireNodeClickEvent(final TreeNode node)
    {
       tree.config().triggerNode = node.value.nodeId;
       
@@ -1355,24 +1373,6 @@
    }
 
    /**
-    * Activates node tooltip.
-    *
-    * @param   node
-    *          The target node.
-    * @param   point
-    *          The location where to activate the tooltip.
-    */
-   private void activateTooltip(TreeGuiImpl.TreeNode<N>  node, NativePoint point)
-   {
-      if (node != null && node.value.tooltip != null && !node.value.tooltip.isEmpty())
-      {
-         ToolTip tt = ToolTip.getInstance();
-         tt.activate(this, node.value.tooltip, -1, point);
-         tooltipNode = node;
-      }
-   }
-
-   /**
     * Deactivates an active tooltip.
     */
    private void deactivateTooltip()
@@ -1388,27 +1388,93 @@
    }
 
    /**
-    * Provides node information, given a point. 
-    * 
-    * @param   point 
-    *          given point
-    * 
-    * @return  Node under the mouse cursor or <code>null</code> if not hit
-    */
-   private TreeGuiImpl.TreeNode<N>  hitTest(NativePoint point)
-   {
-      return isNodeContentAtLocation(point);
-   }
-
-   /**
     * Set mouse cursor.
     * 
     * @param   cursor
     *          Mouse cursor
     */
-   private void setMouseCursor(CursorType cursor)
+   private void setMouseCursor(final CursorType cursor)
    {
-      MousePtrWrapper mousePointerWrapper = MousePtrWrapper.createPreDefinedInstance(cursor);
+      final MousePtrWrapper mousePointerWrapper = MousePtrWrapper.createPreDefinedInstance(cursor);
       gd.setCursor(mousePointerWrapper);
    }
+
+   @Override
+   public BaseConfig config()
+   {
+      return config;
+   }
+   
+   private final int getVisibleNodeCount()
+   {
+      final NodesIterator it = new NodesIterator(tree.rootNode);
+      int count = 0;
+      while (it.hasNext())
+      {
+         it.next();
+         count++;
+      }
+      return count;
+   }
+
+   /**
+    * Update the vertical scrolling model based on this object geometry and contents.
+    * 
+    * @return {@code true} if model was changed as the result of the call.
+    */
+   @Override
+   public boolean updateVScrollModel()
+   {
+      final int widgetHeightPixels = screen().coordinates().heightToNative(height());
+      final int rowHeight = getNodeHeight();
+      final int modelPage = widgetHeightPixels / rowHeight;
+      final int modelWidth = getVisibleNodeCount();
+
+      final ScrollModel vModel = tree.vModel;
+      int newPosition = vModel.getPosition();
+
+      /**
+       * We need to assure the starting position is valid.
+       */
+      newPosition = Math.max(0, Math.min(newPosition, modelWidth - 1));
+
+      /**
+       * Count how many rows fully visible starting from current top
+       * row and below.
+       */
+      
+      // Rows starting from the top and below
+      final int rowsFromTop = modelWidth - newPosition;
+      // Rows clipped below the view
+      final int rowsClippedBelow = rowsFromTop - modelPage;
+      if (rowsClippedBelow >= 0)
+      {
+         // View is filled fully
+         return vModel.set(modelWidth, modelPage, newPosition);         
+      }
+
+      // Some rows are empty, try to "scroll up" to display as many rows as possible
+      newPosition = Math.max(0, newPosition + rowsClippedBelow);
+      return vModel.set(modelWidth, modelWidth - newPosition, newPosition);
+   }
+
+   /**
+    * This procedure is called by the model after it was changed.
+    * 
+    * @param model the model that was changed
+    */
+   @Override
+   public void modelChanged(Model model)
+   {
+      if (model == tree.hModel)
+      {
+         updateHScrollModel();
+      }
+      else
+      {
+         final TreeNodeEntry value = getTopNode().value;
+         tree.config().topNode = value != null ? value.nodeId : -1;
+      }
+      repaint();               
+   }
 }

=== modified file 'src/com/goldencode/p2j/ui/client/gui/TreeGuiImpl.java'
--- src/com/goldencode/p2j/ui/client/gui/TreeGuiImpl.java	2021-06-09 13:50:54 +0000
+++ src/com/goldencode/p2j/ui/client/gui/TreeGuiImpl.java	2021-06-17 18:05:43 +0000
@@ -92,27 +92,28 @@
 */
 package com.goldencode.p2j.ui.client.gui;
 
+import java.util.*;
+import java.util.function.*;
+
+import org.apache.commons.collections.iterators.*;
+
 import com.goldencode.p2j.ui.*;
 import com.goldencode.p2j.ui.TreeNodeEntry.*;
 import com.goldencode.p2j.ui.chui.*;
 import com.goldencode.p2j.ui.client.*;
 import com.goldencode.p2j.ui.client.border.*;
-import com.goldencode.p2j.ui.client.driver.*;
 import com.goldencode.p2j.ui.client.event.*;
 import com.goldencode.p2j.ui.client.event.listener.*;
-import com.goldencode.p2j.ui.client.gui.GuiFontResolver.FontCache;
+import com.goldencode.p2j.ui.client.gui.GuiFontResolver.*;
 import com.goldencode.p2j.ui.client.gui.driver.*;
-import com.goldencode.p2j.ui.client.gui.theme.Theme;
-import com.goldencode.p2j.ui.client.gui.theme.ThemeManager;
-import com.goldencode.p2j.ui.client.layout.BorderLayout;
+import com.goldencode.p2j.ui.client.gui.theme.*;
+import com.goldencode.p2j.ui.client.layout.*;
+import com.goldencode.p2j.ui.client.model.*;
 import com.goldencode.p2j.ui.client.widget.*;
 import com.goldencode.p2j.util.*;
-
-import org.apache.commons.collections.iterators.*;
-
-import java.util.*;
-import java.util.function.*;
-
+import com.goldencode.util.*;
+import com.google.common.base.MoreObjects.*;
+import com.google.common.collect.*;
 
 /**
  * Base class for client-side implementations of TREEVIEW and TREELIST widgets.
@@ -123,19 +124,16 @@
  * @param <N>
  *        node type 
  */
-public abstract class TreeGuiImpl<
-            C extends TreeConfig<N>,
-            B extends TreeBodyGuiImpl<C, ? extends TreeGuiImpl<C,B,N>, N>,
-            N extends TreeNodeEntry>
+public abstract class TreeGuiImpl
 extends BorderedPanelGuiImpl
-implements WidgetWithConfig<C>,
+implements WidgetWithConfig<TreeConfig>,
            DataContainer
 {
    /** Associated config instance */
-   protected C config;
+   protected TreeConfig config;
 
    /** Screen driver */
-   protected GuiDriver<?,?> gd;
+   protected final GuiDriver<?,?> gd = (GuiDriver<?,?>) OutputManager.getDriver();
 
    /** Font resolver */
    protected GuiFontResolver gf;
@@ -143,39 +141,47 @@
    /** Color resolver */
    protected GuiColorResolver gc;
 
-   /** ScrollPane which wraps the tree items and implements scrolling. */
-   protected ScrollPane<GuiOutputManager> scrollpane;
+   /**
+    * The scrolling provider.
+    */
+   protected GenericContainer scrollableContainer;
 
    /** Tree view body. */
-   protected B body = null;
+   public TreeBodyGuiImpl body = null;
 
    /** Currently selected node */
-   protected TreeNode<N> currentNode;
+   protected TreeNode currentNode;
 
    /** Current drop highlight node */
-   protected TreeNode<N> dropHighlightNode;
+   protected TreeNode dropHighlightNode;
 
    /** The root node */
-   protected TreeNode<N> rootNode;
+   protected TreeNode rootNode;
 
    /** Widget id */
    private WidgetId id;
 
    /** Map of node ids to nodes */
-   private HashMap<Integer, TreeNode<N>> idNodes;
+   private HashMap<Integer, TreeNode> idNodes;
 
    /** The tree label editor */
-   public TreeEditor<?, C> treeEditor;
-
+   public TreeEditor treeEditor;
+
+   /** Horizontal scroll model */
+   final ScrollModel hModel = new ScrollModel();
+
+   /** Horizontal scroll model */
+   final ScrollModel vModel = new ScrollModel();
+   
    /**
-    * Construtor.
+    * Constructor.
     * 
     * @param   id
     *          Widget id.
     * @param   cfg
     *          Config instance.
     */
-   public TreeGuiImpl(WidgetId id, C cfg)
+   public TreeGuiImpl(final WidgetId id, final TreeConfig cfg)
    {
       this.id = id;
       this.config = cfg;
@@ -193,43 +199,28 @@
     *          Config instance.
     */
    @Override
-   public void initialize(WidgetId id, C cfg)
+   public void initialize(final WidgetId id, final TreeConfig cfg)
    {
       config = ConfigManager.getInstance().resolveWidgetConfig(id, cfg);
 
-      // screen driver
-      if (gd == null)
-      {
-         gd = (GuiDriver<?,?>) OutputManager.getDriver();
-      }
-      // and font resolver
-      if (gf == null)
-      {
-         gf = new GuiFontResolver(WindowManager.resolveWindow(this), gd, config, false);
-      }
+      gf = new GuiFontResolver(WindowManager.resolveWindow(this), gd, config, false);
 
-      int bgColor = isThreeD() && !config.enabled ? SysColor.COLOR_3DFACE : SysColor.COLOR_WINDOW;
+      final int bgColor = isThreeD() && !config.enabled ? SysColor.COLOR_3DFACE : SysColor.COLOR_WINDOW;
       
       gc = new GuiColorResolver(gd, config, bgColor, SysColor.COLOR_BTNTEXT);
       
+      // FIXME: is this correct?
       setBorder(new LineBorderGuiImpl(null, gc, null));
 
-      WidgetFactory<GuiOutputManager> factory = screen().getFactory();
-
+      final WidgetRegistry<GuiOutputManager> registry = screen().getRegistry();
       body = createBody();
-      
-      scrollpane = factory.createScrollPane(body);
-      scrollpane.getRootPanel().setTransparent(true);
-      scrollpane.setScrollBars(ScrollBar.Bars.BOTH);
-
-      add(scrollpane);
-
-      WidgetRegistry<GuiOutputManager> registry = screen().getRegistry();
-      
-      registry.addWidget(scrollpane);
       registry.addWidget(body);
-      
-      treeEditor = new TreeEditor<>(gd, screen(), screen().coordinates(), this, null, null, gc, gf);
+
+      scrollableContainer = new ScrollableContainer(body, hModel, vModel);
+      add(scrollableContainer);      
+      registry.addWidget(scrollableContainer);
+
+      treeEditor = new TreeEditor(gd, screen(), screen().coordinates(), this, null, null, gc, gf);
       add(treeEditor);
    }
 
@@ -254,7 +245,7 @@
     * @return  Reference to widget config.
     */
    @Override
-   public C config()
+   public TreeConfig config()
    {
       return config;
    }
@@ -276,69 +267,61 @@
     *          Config reference capturing the config state before the owner's config was modified.
     */
    @Override
-   public void afterConfigUpdate(C beforeUpdate)
+   public void afterConfigUpdate(TreeConfig beforeUpdate)
    {
       super.afterConfigUpdateBase(beforeUpdate);
 
-      if (body != null)
-      {
-         Integer topNodeId = null;
-         
-         if (config.topNode != beforeUpdate.topNode && config.topNode > 0)
-         {
-            topNodeId = config.topNode;
-         }
-         // the color values can be changed even if the assignFlags is the same
-         if (config.selectedNodeBg != beforeUpdate.selectedNodeBg ||
-             config.selectedNodeFg != beforeUpdate.selectedNodeFg)
-         {
-            body.updateColors(config);
-         }
-         
-         buildTreeModel();
-         
-         if (topNodeId != null)
-         {
-            TreeGuiImpl.TreeNode<N>  topNode = findNode(topNodeId);
-            toggleNodeExpansion(topNode, true);
-            body.scrollToTop(topNodeId);
-         }
-         
-         if (isLabelsEditingSupported() && idNodes.containsKey(config.selectedNodeId))
-         {
-            TreeNode<N> selectedNode = idNodes.get(config.selectedNodeId);
-            
-            if (selectedNode.value.editorMode == EditorMode.ACTIVATING)
-            {
-               //config.nodes.indexOf(selectedNode.value) != -1
-               //selectedNode.value is a reference to some node entry
-               if (fireBeforeLabelEdit(selectedNode))
-               {
-                  selectedNode.value.editorMode = EditorMode.INACTIVE;
-                  treeEditor.deactivateEditor();
-               }
-               else
-               {
-                  selectedNode.value.editorMode = EditorMode.ACTIVE;
-                  
-                  getBody().ensureVisible(selectedNode, false, false);
-                  
-                  NativeRectangle labelBounds = getNodeLabelBounds(selectedNode);
-                  
-                  treeEditor.activateEditor(labelBounds.left(),
-                                            labelBounds.top(),
-                                            labelBounds.width(),
-                                            labelBounds.height(),
-                                            (String) selectedNode.value.getValue(0));
-                  treeEditor.requestFocus(true);
-               }
-               
-               ConfigHelper.setModified(config, true);
-            }
-         }
-         
-         repaint();
-      }
+      final Integer topNodeId = (config.topNode != beforeUpdate.topNode && config.topNode > 0)
+               ? config.topNode
+               : null;
+      // the color values can be changed even if the assignFlags is the same
+      if (config.selectedNodeBg != beforeUpdate.selectedNodeBg
+               || config.selectedNodeFg != beforeUpdate.selectedNodeFg)
+      {
+         body.updateColors(config);
+      }
+
+      buildTreeModel();
+
+      if (topNodeId != null)
+      {
+         final TreeNode topNode = findNode(topNodeId);
+         toggleNodeExpansion(topNode, true);
+         body.scrollToTop(topNodeId);
+      }
+
+      if (isLabelsEditingSupported() && idNodes.containsKey(config.selectedNodeId))
+      {
+         final TreeNode selectedNode = idNodes.get(config.selectedNodeId);
+
+         if (selectedNode.value.editorMode == EditorMode.ACTIVATING)
+         {
+            // config.nodes.indexOf(selectedNode.value) != -1
+            // selectedNode.value is a reference to some node entry
+            if (fireBeforeLabelEdit(selectedNode))
+            {
+               selectedNode.value.editorMode = EditorMode.INACTIVE;
+               treeEditor.deactivateEditor();
+            }
+            else
+            {
+               selectedNode.value.editorMode = EditorMode.ACTIVE;
+
+               getBody().ensureVisible(selectedNode, false, false);
+
+               NativeRectangle labelBounds = getNodeLabelBounds(selectedNode);
+
+               treeEditor.activateEditor(labelBounds.left(), labelBounds.top(),
+                        labelBounds.width(), labelBounds.height(),
+                        (String) selectedNode.value.getValue(0));
+               treeEditor.requestFocus(true);
+            }
+
+            ConfigHelper.setModified(config, true);
+         }
+      }
+
+      repaint();
    }
 
    /**
@@ -349,9 +332,9 @@
     * 
     * @return   The node label's bounds
     */
-   public NativeRectangle getNodeLabelBounds(TreeNode<?> node)
+   public NativeRectangle getNodeLabelBounds(final TreeNode node)
    {
-      int level = getNodeLevel(node);
+      int level = node.getLevel();
       
       int rowIndex = getNodeRowIndex(node);
       
@@ -387,7 +370,7 @@
     * 
     * @return   True if the trigger code returns NO-APPLY, otherwise false.
     */
-   public boolean fireBeforeLabelEdit(TreeGuiImpl.TreeNode<?> node)
+   public boolean fireBeforeLabelEdit(final TreeNode node)
    {
       config().triggerNode = node.value.nodeId;
       
@@ -408,7 +391,7 @@
     * 
     * @return   True if the trigger code returns NO-APPLY, otherwise false.
     */
-   public boolean fireAfterLabelEdit(TreeGuiImpl.TreeNode<?> node, String newLabel)
+   public boolean fireAfterLabelEdit(final TreeNode node, final String newLabel)
    {
       config().triggerNode = node.value.nodeId;
       config().newLabel = newLabel;
@@ -440,7 +423,7 @@
    @Override
    public void setValue(BaseDataType value)
    {
-      TreeNode<N> node = null;
+      TreeNode node = null;
       if (value != null && !value.isUnknown())
       {
          node = findFirstNodeWithText(value.toStringMessage());
@@ -565,7 +548,7 @@
     *
     * @return  see above.
     */
-   public TreeNode<N> getCurrentNode()
+   public TreeNode getCurrentNode()
    {
       return currentNode;
    }
@@ -575,7 +558,7 @@
     *
     * @return   The drop highlight node.
     */
-   public TreeNode<N> getDropHighlightNode()
+   public TreeNode getDropHighlightNode()
    {
       return dropHighlightNode;
    }
@@ -585,7 +568,7 @@
     *
     * @return  see above.
     */
-   public TreeNode<N> getRootNode()
+   public TreeNode getRootNode()
    {
       return rootNode;
    }
@@ -595,7 +578,7 @@
     *
     * @return  see above.
     */
-   public B getBody()
+   public TreeBodyGuiImpl getBody()
    {
       return body;
    }
@@ -669,7 +652,7 @@
     * 
     * @return   The tree node editor
     */
-   public TreeEditor<?, C> getTreeEditor()
+   public TreeEditor getTreeEditor()
    {
       return treeEditor;
    }
@@ -684,7 +667,7 @@
     * 
     * @return   The image
     */
-   public final ImageWrapper<?> getNodeImage(final TreeNode<?> node, final boolean nodeSelected)
+   public final ImageWrapper<?> getNodeImage(final TreeNode node, final boolean nodeSelected)
    {
       final int imgId = node.getNodeImageId(nodeSelected);
       if (imgId < 0)
@@ -733,7 +716,7 @@
     *
     * @return  the node text.
     */
-   protected abstract String getNodeText(TreeNode<N> node);
+   protected abstract String getNodeText(final TreeNode node);
 
 
    /**
@@ -742,7 +725,7 @@
     * @param   nodes
     *          List of nodes.
     */
-   protected abstract void sortNodes(List<? extends N> nodes);
+   protected abstract void sortNodes(final List<TreeNodeEntry> nodes);
 
    /**
     * Updates a tree node with the backing configuration entry.
@@ -752,7 +735,7 @@
     * @param   nodeEntry
     *          Configuration entry.
     */
-   protected void updateTreeNode(TreeNode<N> node, N nodeEntry)
+   protected static void updateTreeNode(final TreeNode node, final TreeNodeEntry nodeEntry)
    {
       node.value = nodeEntry;
    }
@@ -762,14 +745,14 @@
     *
     * @return  valid body instance.
     */
-   protected abstract B createBody();
+   protected abstract TreeBodyGuiImpl createBody();
 
    /**
     * Builds tree model from the config's flat list.
     */
    protected void buildTreeModel()
    {
-      final C c = config();
+      final TreeConfig c = config();
       if (c.selectedNodeId <= 0)
       {
          final TreeNodeEntry selected = currentNode != null ? currentNode.value : null;
@@ -785,19 +768,18 @@
       }
       
       idNodes = new HashMap<>();
-      final TreeNode<N> root = new TreeNode<>();
+      final TreeNode root = new TreeNode();
 
+      final List<TreeNodeEntry> nodes = c.nodes;
       if (c.sorted)
       {
-         sortNodes(c.nodes);
+         sortNodes(nodes);
       }
 
-      for (int i = 0; i < c.nodes.size(); i++)
+      for (final TreeNodeEntry n : nodes)
       {
-         final N n = c.nodes.get(i);
-         final TreeNode<N> node = new TreeNode<>();
+         final TreeNode node = new TreeNode();
          updateTreeNode(node, n);
-
          idNodes.put(n.nodeId, node);
 
          // restore selection
@@ -812,10 +794,9 @@
          }
      }
 
-      for (int i = 0; i < c.nodes.size(); i++)
+      for (final TreeNodeEntry n : nodes)
       {
-         final N n = c.nodes.get(i);
-         final TreeNode<N> node = idNodes.get(n.nodeId);
+         final TreeNode node = idNodes.get(n.nodeId);
          final int parentId = n.parentId;
          if (parentId != Integer.MIN_VALUE)
          {
@@ -838,6 +819,8 @@
 
       rootNode = root;
       body.calcSize();
+      body.updateVScrollModel();
+      body.updateHScrollModel();
       repaint();
    }
 
@@ -872,12 +855,12 @@
     *
     * @return  a tree node instance or null if no node found.
     */
-   TreeNode<N> findFirstNodeWithText(String nodeText)
+   TreeNode findFirstNodeWithText(String nodeText)
    {
-      NodesIterator<N> it = new NodesIterator<N>(rootNode, false);
+      final NodesIterator it = new NodesIterator(rootNode, false);
       while (it.hasNext())
       {
-         TreeNode<N> node = it.next();
+         TreeNode node = it.next();
          if (getNodeText(node).equalsIgnoreCase(nodeText))
          {
             return node;
@@ -893,7 +876,7 @@
     * @param   node
     *          The node to set as current.
     */
-   public void setCurrentNode(final TreeNode<N> node)
+   public void setCurrentNode(final TreeNode node)
    {
       setCurrentNode(node, true, true);
    }
@@ -908,7 +891,7 @@
     * @param   direct
     *          If {@code true}, the method will post the input event {@link Keyboard#SE_CHANGE_NODE_DIRECT}.
     */
-   public void setCurrentNode(final TreeNode<N> node, boolean trigger, boolean direct)
+   public void setCurrentNode(final TreeNode node, boolean trigger, boolean direct)
    {
       if (currentNode == node)
       {
@@ -917,7 +900,7 @@
 
       getTreeEditor().deactivateEditor();
 
-      final C c = config();
+      final TreeConfig c = config();
       final int prevNode = c.selectedNodeId;
 
       if (node == null)
@@ -929,7 +912,7 @@
       else
       {
 
-         TreeNode<N> parent = node.parent;
+         TreeNode parent = node.parent;
          while (parent != null && parent.value != null)
          {
             parent.value.expanded = true;
@@ -967,7 +950,7 @@
     *
     * @return  the index.
     */
-   int getNodeRowIndex(TreeNode<?> node)
+   int getNodeRowIndex(final TreeNode node)
    {
       return getNodeRowIndex(node, true);
    }
@@ -984,10 +967,10 @@
     * 
     * @return  the index.
     */
-   int getNodeRowIndex(TreeNode<?> node, boolean excludeCollapsedNodes)
+   int getNodeRowIndex(final TreeNode node, final boolean excludeCollapsedNodes)
    {
       int res = 0;
-      NodesIterator<?> it = new NodesIterator<>(rootNode, excludeCollapsedNodes);
+      NodesIterator it = new NodesIterator(rootNode, excludeCollapsedNodes);
       while (it.hasNext())
       {
          if (it.next() == node)
@@ -1013,10 +996,10 @@
     * 
     * @return  the index.
     */
-   int getNodeRowIndex(int nodeId, boolean excludeCollapsedNodes)
+   int getNodeRowIndex(final int nodeId, final boolean excludeCollapsedNodes)
    {
       int res = 0;
-      NodesIterator<?> it = new NodesIterator<>(rootNode, excludeCollapsedNodes);
+      final NodesIterator it = new NodesIterator(rootNode, excludeCollapsedNodes);
       while (it.hasNext())
       {
          if (it.next().value.nodeId == nodeId)
@@ -1038,33 +1021,12 @@
     *
     * @return  {@link TreeNode} or {@code null} when no node found.
     */
-   TreeNode<N> findNode(int nodeId)
+   TreeNode findNode(final int nodeId)
    {
       return idNodes.get(nodeId);
    }
 
    /**
-    * Returns the zero based node level.
-    *
-    * @param   node
-    *          The level to test.
-    *
-    * @return  the node level.
-    */
-   int getNodeLevel(TreeNode<?> node)
-   {
-      int result = -1;
-      TreeNode<?> parent = node.parent;
-      while (parent != null)
-      {
-         result++;
-         parent = parent.parent;
-      }
-
-      return result;
-   }
-
-   /**
     * Returns {@code true} if the tree has at least one node.
     *
     * @return  see above.
@@ -1079,7 +1041,7 @@
     */
    void toggleCurrentNodeExpansion()
    {
-      final TreeGuiImpl.TreeNode<?> currentNode = getCurrentNode();
+      final TreeNode currentNode = getCurrentNode();
       if (currentNode != null)
       {
          toggleNodeExpansion(currentNode);
@@ -1092,7 +1054,7 @@
     * @param   node
     *          The node to expand or collapse.
     */
-   void toggleNodeExpansion(TreeNode<?> node)
+   void toggleNodeExpansion(final TreeNode node)
    {
       if (node == null)
       {
@@ -1110,7 +1072,7 @@
     * @param   expand
     *          The desired expansion state.
     */
-   protected void toggleNodeExpansion(final TreeNode<?> node, final boolean expand)
+   protected void toggleNodeExpansion(final TreeNode node, final boolean expand)
    {
       // We have no means to call root node expand/collapse, so we are assuming the node is not
       // the root node
@@ -1120,7 +1082,7 @@
          return;
       }
 
-      final C c = config();
+      final TreeConfig c = config();
       final int nodeId = value.nodeId;
       c.triggerNode = nodeId;
 
@@ -1136,7 +1098,7 @@
       }
 
       // the nodes may have been rebuilt, retrieve the node from the model again
-      TreeNode<N> n = findNode(nodeId);
+      TreeNode n = findNode(nodeId);
       if (n == null)
       {
          return;
@@ -1145,7 +1107,7 @@
       // The node cannot be root, so there is no need to test the value for nullness
       if (expand)
       {
-         /** Make sure all ancestors are expanded too */
+         // Make sure all ancestors are expanded too
          while (n != null && n.value != null)
          {
             n.value.expanded = true;
@@ -1163,7 +1125,8 @@
       fireNodeExpansionTrigger(c,
                expand ? Keyboard.SE_NODE_EXPANDED : Keyboard.SE_NODE_COLLAPSED);
 
-      body.calcSize();
+      // body.calcSize();
+      body.updateHScrollModel();
       repaint();
    }
 
@@ -1173,7 +1136,7 @@
     * @param   cfgTree
     *          The tree widget config.
     */
-   protected void makeNodeConfigDirty(C cfgTree)
+   protected static void makeNodeConfigDirty(final TreeConfig cfgTree)
    {
       ConfigManager.getInstance().addDirtyConfig(cfgTree, "nodes");
    }
@@ -1184,19 +1147,21 @@
     * 
     * @return   Boolean value that indicates label's editing is supported or not.
     */
+   @SuppressWarnings("static-method")
    protected boolean isLabelsEditingSupported()
    {
       return false;
    }
 
    /**
-    * Returns the number of visible nodes that will fully fit in the widget's viewport.
+    * Returns the number of visible nodes that will at least partly fit in the widget's viewport.
     *
     * @return  see above.
     */
    int getVisibleNodeCount()
    {
-      return scrollpane.viewport().physicalDimension().height / body.getNodeHeight();
+      final int nodeHeight = body.getNodeHeight();
+      return (body.physicalDimension().height + nodeHeight - 1) / nodeHeight;
    }
 
    /**
@@ -1219,7 +1184,7 @@
     *
     * @return  <code>TRUE</code> if trigger result consumed, <code>FALSE</code> otherwise.
     */
-   protected boolean fireNodeExpansionTrigger(C cfgTree, int eventCode)
+   protected boolean fireNodeExpansionTrigger(TreeConfig cfgTree, int eventCode)
    {
       makeNodeConfigDirty(cfgTree);
       
@@ -1235,16 +1200,17 @@
     * @param <N> 
     *        node type
     */
-   public static class TreeNode<N extends TreeNodeEntry>
+   public static class TreeNode
+   extends AbstractPrintable
    {
       /** Parent node */
-      public TreeNode<N> parent;
+      public TreeNode parent;
 
       /** Child nodes or {@code null} if case of no children */
-      public ArrayList<TreeNode<N>> nodes;
+      public List<TreeNode> nodes;
 
       /** Related config node entry */
-      public N value;
+      public TreeNodeEntry value;
 
       /**
        * Adds a node to the list of child nodes.
@@ -1253,7 +1219,7 @@
        * @param   node
        *          The node to add.
        */
-      public void addNode(TreeNode<N> node)
+      public void addNode(TreeNode node)
       {
          if (nodes == null)
          {
@@ -1293,11 +1259,175 @@
        *        
        * @return see above
        */
-      public boolean isDescendantOf(final TreeNode<N> other)
+      public boolean isDescendantOf(final TreeNode other)
       {
          return parent == other || (parent != null && parent.isDescendantOf(other));
       }
 
+      public int childIndex()
+      {
+         return parent.nodes.indexOf(this);
+      }
+      
+      /**
+       * Get list consisting of this node and of ancestors of this node, starting from the tree root.
+       * 
+       * @return see above
+       */
+      private final List<TreeNode> ancestorsAndThis()
+      {
+         if(parent == null)
+         {
+            return Lists.newArrayList(this);
+         }
+         final List<TreeNode> result = parent.ancestorsAndThis();
+         result.add(this);
+         return result;
+      }
+
+      /**
+       * Test if this node is strictly lower than other sibling node in sibling order.
+       *  
+       * @param other
+       *        other node to compare, must be a sibling to this node, and differ from this node
+       *        
+       * @return see above
+       */
+      private final boolean isSiblingLowerThan(final TreeNode other)
+      {
+         assert parent != null;
+         assert other.parent == parent;
+         final List<TreeNode> siblings = parent.nodes;
+         for (TreeNode n : siblings)
+         {
+            if (n == this)
+            {
+               return true;
+            }
+            if (n == other)
+            {
+               return false;
+            }
+         }
+         throw new IllegalStateException();
+      }
+      
+      /**
+       * Test if this node precedes a given node in tree depth first traversal order.
+       * 
+       * Both nodes must not be null, the same node, or tree root.
+       * 
+       * @param other
+       *        the node to compare with
+       *        
+       * @return see above.
+       */
+      public boolean isDFTBeforeThan(final TreeNode other)
+      {
+         assert other != null;
+         assert this != other;
+         assert parent != null;
+         assert other.parent != null;
+
+         final List<TreeNode> thisAncestors = ancestorsAndThis();
+         final List<TreeNode> otherAncestors = other.ancestorsAndThis();
+         final int thisAncestorsSize = thisAncestors.size();
+         final int minLength = Math.min(thisAncestorsSize, otherAncestors.size());
+
+         // Nodes belong to the same tree, so have at least common ancestor,
+         // and both list lengths are at least 2.
+         for (int lcoIdx = 1;; lcoIdx++)
+         {
+            TreeNode n = thisAncestors.get(lcoIdx);
+            TreeNode o = otherAncestors.get(lcoIdx);
+            if (n != o)
+            {
+               return n.isSiblingLowerThan(o);
+            }
+            if (lcoIdx == minLength - 1)
+            {
+               if (thisAncestorsSize > minLength)
+               {
+                  final TreeNode n2 = thisAncestors.get(minLength -1);
+                  if (n2 == other)
+                  {
+                     // this node descends from the other
+                     return false;
+                  }
+                  return !other.isSiblingLowerThan(n2);
+               }
+               final TreeNode o2 = otherAncestors.get(minLength -1);
+               if (o2 == this)
+               {
+                  // other node descends from this node
+                  return true;
+               }
+               return this.isSiblingLowerThan(o2);
+            }
+         }
+      }
+
+      /**
+       * Returns the zero based node level.
+       * The level of the root (invisible) node is -1.
+       *
+       * @return  the node level.
+       */
+      public int getLevel()
+      {
+         return parent != null ? parent.getLevel() + 1 : -1;
+      }
+
+      /**
+       * Get the next sibling of this node or {@code null} if this node is the last child.
+       * 
+       * @return see above
+       */
+      public final TreeNode nextSibling()
+      {
+         if (parent == null)
+         {
+            // I am root, I have no siblings
+            return null;
+         }
+         
+         final List<TreeNode> meAndSiblings = parent.nodes;
+         assert meAndSiblings != null;
+         final int myIdx = meAndSiblings.indexOf(this);
+         assert myIdx >= 0; // otherwise the hierarchy is broken
+         return myIdx < meAndSiblings.size() - 1 ? meAndSiblings.get(myIdx + 1) : null;
+      }
+      
+      private TreeNode nextSiblingRecursively()
+      {
+         final TreeNode sibling = nextSibling();
+         return sibling != null ? sibling
+                  : parent == null ? null : parent.nextSiblingRecursively();
+      }
+      
+      /**
+       * Get the next visible node in the hierarchy
+       * 
+       * @return the next visible node or {@code null} if this node is the last visible node.
+       */
+      public TreeNode nextVisible()
+      {
+         if (parent == null)
+         {
+            // This node is the root node, which is never visible
+            return null;
+         }
+
+         if (value.expanded && nodes != null)
+         {
+            // This node is expanded, and there is at least one child, return it
+            return nodes.get(0);
+         }
+
+         final TreeNode nextSibling = nextSibling();
+         return nextSibling != null ? nextSibling : parent.nextSiblingRecursively();
+      }
+      
       /**
        * Return the internal image identifier.
        *  
@@ -1308,24 +1438,13 @@
        */
       public final int getNodeImageId(final boolean nodeSelected)
       {
+         if (nodeSelected && value.selectedIconId != -1)
+         {
+            return value.selectedIconId;
+         }
+
          // node image if defined
-         int imgId = value.expanded && value.expandIconId != -1 ?
-                     value.expandIconId :
-                     value.iconId;
-         
-         if(nodeSelected)
-         {
-            if (value.selectedIconId != -1)
-            {
-               imgId = value.selectedIconId;
-            }
-            else
-            {
-               imgId = value.iconId;
-            }
-         }
-         
-         return imgId;
+         return value.expanded && value.expandIconId != -1 ? value.expandIconId : value.iconId;
       }
 
       /**
@@ -1336,7 +1455,7 @@
        * 
        * @return  index node in the list or <code>-1</code> if not found in list.
        */
-      private int check(TreeNode<N> node)
+      private int check(TreeNode node)
       {
          for (int i=0;i < nodes.size(); i++)
          {
@@ -1348,16 +1467,36 @@
          
          return -1;
       }
+
+      /**
+       * Add class-specific fields to the helper.
+       * 
+       * @param stringHelper the string helper to update
+       */
+      @Override
+      protected void updateStringHelper(final ToStringHelper stringHelper)
+      {
+         stringHelper.add("level", getLevel());
+         if (nodes != null)
+         {
+            stringHelper.addValue(nodes.size() + " children");
+         }
+
+         if (value != null)
+         {
+            value.updateStringHelper(stringHelper);
+         }
+      }
    }
 
    /**
     * Recursive nodes iterator.
     */
-   protected static class NodesIterator<T extends TreeNodeEntry>
-   implements Iterator<TreeNode<T>>
+   public static class NodesIterator
+   implements Iterator<TreeNode>
    {
       /** stack of iterators of individual node levels */
-      private Stack<ListIterator<TreeNode<T>>> stack = new Stack<>();
+      private java.util.Stack<ListIterator<TreeNode>> stack = new java.util.Stack<>();
 
       /** whether the unexpanded nodes should be iterated, too */
       private boolean visibleOnly;
@@ -1371,7 +1510,7 @@
        * @param   root
        *          Where to start the iteration.
        */
-      public NodesIterator(TreeNode<T> root)
+      public NodesIterator(final TreeNode root)
       {
          this(root, true);
       }
@@ -1384,7 +1523,7 @@
        * @param   visibleOnly
        *          Whether the unexpanded nodes should be iterated, too.
        */
-      public NodesIterator(TreeNode<T> root, boolean visibleOnly)
+      public NodesIterator(TreeNode root, boolean visibleOnly)
       {
          this(root, visibleOnly, false);
       }
@@ -1399,7 +1538,7 @@
        * @param   reverse
        *          Reverse order.
        */
-      public NodesIterator(TreeNode<T> root, boolean visibleOnly, boolean reverse)
+      public NodesIterator(final TreeNode root, final boolean visibleOnly, final boolean reverse)
       {
          this.reverse = reverse;
 
@@ -1408,7 +1547,6 @@
          {
             stack.push(reverse ? new ReverseListIterator(root.nodes) : root.nodes.listIterator());
          }
-
          this.visibleOnly = visibleOnly;
       }
 
@@ -1422,7 +1560,7 @@
       @Override
       public boolean hasNext()
       {
-         for (Iterator<TreeNode<T>> it : stack)
+         for (Iterator<TreeNode> it : stack)
          {
             if (it.hasNext())
             {
@@ -1440,7 +1578,7 @@
        * @throws  NoSuchElementException if the iteration has no more elements
        */
       @Override
-      public TreeNode<T> next()
+      public TreeNode next()
       {
          if (stack.isEmpty())
          {
@@ -1454,14 +1592,14 @@
             ascend = true;
          }
 
-         ListIterator<TreeNode<T>> it = stack.isEmpty() ? null : stack.peek();
+         ListIterator<TreeNode> it = stack.isEmpty() ? null : stack.peek();
 
          if (it == null || !it.hasNext())
          {
             throw new NoSuchElementException();
          }
 
-         TreeNode<T> node = it.next();
+         TreeNode node = it.next();
          if (reverse)
          {
             while (!ascend &&
@@ -1491,7 +1629,7 @@
    /**
     * Represents the tree node editor.
     */
-   protected static class TreeEditor<N extends TreeNodeEntry, C extends TreeConfig<N>>
+   protected static class TreeEditor
    extends BorderedPanelGuiImpl
    {
       /** Output manager */
@@ -1501,7 +1639,7 @@
       private final CoordinatesConversion cc;
       
       /** The parent container */
-      private final TreeGuiImpl<C,?,?> owner;
+      private final TreeGuiImpl owner;
       
       /** The consumer of the edited value */
       private final Consumer<String> valueConsumer;
@@ -1510,7 +1648,7 @@
       private final Runnable onHideCallback;
       
       /** The related node */
-      private TreeNode<?> editedNode;
+      private TreeNode editedNode;
 
       private int widthInPixels;
 
@@ -1545,7 +1683,7 @@
       public TreeEditor(GuiDriver<?,?> gd,
                         GuiOutputManager screen,
                         CoordinatesConversion cc,
-                        TreeGuiImpl<C,?,?> owner,
+                        TreeGuiImpl owner,
                         Consumer<String> valueConsumer,
                         Runnable onHideCallback,
                         GuiColorResolver colorResolver,
@@ -1774,7 +1912,7 @@
             
             owner.setState(ScreenBuffer.CHANGED);
             
-            owner.makeNodeConfigDirty(owner.config());
+            TreeGuiImpl.makeNodeConfigDirty(owner.config());
             
             setVisible(false);
             owner.setFocusInt(owner.defaultFocus());

=== modified file 'src/com/goldencode/p2j/ui/client/gui/TreeListBodyGuiImpl.java'
--- src/com/goldencode/p2j/ui/client/gui/TreeListBodyGuiImpl.java	2020-09-07 16:23:31 +0000
+++ src/com/goldencode/p2j/ui/client/gui/TreeListBodyGuiImpl.java	2021-06-18 18:40:47 +0000
@@ -66,7 +66,6 @@
 
 package com.goldencode.p2j.ui.client.gui;
 
-import com.goldencode.p2j.ui.*;
 import com.goldencode.p2j.ui.client.*;
 import com.goldencode.p2j.ui.client.gui.TreeGuiImpl.*;
 import com.goldencode.p2j.ui.client.gui.theme.*;
@@ -78,8 +77,8 @@
  * @param <N> 
  *        Node type
  */
-public class TreeListBodyGuiImpl<N extends TreeListNodeEntry>
-extends TreeBodyGuiImpl<TreeListConfig<N>, TreeListGuiImpl<N>, N>
+public class TreeListBodyGuiImpl
+extends TreeBodyGuiImpl
 {
    /**
     * Constructor.
@@ -87,7 +86,7 @@
     * @param treeView
     * The owning tree view.
     */
-   public TreeListBodyGuiImpl(TreeListGuiImpl<N> treeView)
+   public TreeListBodyGuiImpl(TreeGuiImpl treeView)
    {
       super(treeView);
    }
@@ -109,7 +108,7 @@
       NativePoint p = tree.screenPhysicalLocation();
       NativePoint scrollLocation = physicalLocation();
       NativePoint point = new NativePoint(x - p.x,
-               y - p.y - tree.getCaptionNativeHeight() - scrollLocation.y);
+               y - p.y - ((TreeListGuiImpl)tree).getCaptionNativeHeight() - scrollLocation.y);
       
       return point;
    }
@@ -127,18 +126,14 @@
       final NativeDimension dim = physicalDimension();
       final NativeRectangle clip = clipRectangle(p, dim);
 
+      final TreeNode topNode = tree.findNode(tree.config.topNode);
+
       // clippable drawing start
-      gd.draw(p, clip, () -> theme.drawTreeListBody(gd,
-                                                    gc,
-                                                    tree.resolveFontCache(),
-                                                    tree,
-                                                    tree.rootNode.nodes,
-                                                    tree.getCurrentNode(),
-                                                    dim,
-                                                    selectionBack,
-                                                    selectionFore,
-                                                    disabledFore,
-                                                    tree.getColumns()));
+      gd.draw(p, clip, () -> {
+         theme.drawTreeListBody(gd, gc, tree.resolveFontCache(), ((TreeListGuiImpl) tree),
+                  topNode, tree.getCurrentNode(), dim.width, dim.height, selectionBack,
+                  selectionFore);
+      });
    }
 
    /**
@@ -157,8 +152,9 @@
    public void scroll(Integer newXPosition, Integer newYPosition)
    {
       super.scroll(newXPosition, newYPosition);
-      tree.caption.setPhysicalLocation(physicalLocation.x, 0);
-      tree.caption.repaint();
+      final TreeListCaption caption = ((TreeListGuiImpl)tree).caption;
+      caption.setPhysicalLocation(physicalLocation.x, 0);
+      caption.repaint();
    }
 
    /**
@@ -174,9 +170,9 @@
     * @return  the calculated width.
     */
    @Override
-   protected int calcMaxNodesWidth(List<TreeNode<N>> nodes, int maxWidth, int level)
+   protected int calcMaxNodesWidth(final List<TreeNode> nodes, final int maxWidth, final int level)
    {
-      List<TreeListGuiImpl.ColumnCache> columns = tree.getColumns();
+      List<TreeListGuiImpl.ColumnCache> columns = ((TreeListGuiImpl)tree).getColumns();
       int colCount = columns.size();
       int width = (colCount - 1) * ClassicTheme.TREELIST_COLUMN_SPACING;
       for (TreeListGuiImpl.ColumnCache cc : columns)
@@ -191,4 +187,16 @@
 
       return width;
    }
+   /**
+    * Update the horizontal scrolling model based on this object geometry and contents.
+    * 
+    * @return {@code true} if model was changed as the result of the call.
+    */
+   @Override
+   public boolean updateHScrollModel()
+   {
+      // FIXME
+      // assert false;
+      return false;
+   }
 }

=== modified file 'src/com/goldencode/p2j/ui/client/gui/TreeListCaption.java'
--- src/com/goldencode/p2j/ui/client/gui/TreeListCaption.java	2021-05-28 17:57:56 +0000
+++ src/com/goldencode/p2j/ui/client/gui/TreeListCaption.java	2021-06-17 16:15:18 +0000
@@ -80,7 +80,7 @@
 extends AbstractContainer<GuiOutputManager>
 {
    /** Owning tree */
-   private final TreeListGuiImpl<?> tree;
+   private final TreeListGuiImpl tree;
 
    /** GUI driver */
    private final GuiDriver<?,?> gd;
@@ -97,17 +97,17 @@
     * @param   tree
     *          The owning tree widget.
     */
-   public TreeListCaption(TreeListGuiImpl<?> tree)
+   public TreeListCaption(final TreeListGuiImpl tree)
    {
       this.tree = tree;
-      TreeListConfig<?> c = tree.config();
+      final TreeConfig cfg = tree.config();
 
       // initialize screen driver
       gd = (GuiDriver<?,?>) OutputManager.getDriver();
 
       // color initializing
-      int bgColor = isThreeD() && !c.enabled ? SysColor.COLOR_3DFACE : SysColor.COLOR_WINDOW;
-      gc = new GuiColorResolver(gd, c, bgColor, SysColor.COLOR_BTNTEXT);
+      int bgColor = isThreeD() && !cfg.enabled ? SysColor.COLOR_3DFACE : SysColor.COLOR_WINDOW;
+      gc = new GuiColorResolver(gd, cfg, bgColor, SysColor.COLOR_BTNTEXT);
 
       // at this point classic theme is expected
       theme = (ClassicTheme) ThemeManager.getCurrentTheme();
@@ -160,7 +160,7 @@
    @Override
    public double height()
    {
-      GuiFontResolver.FontCache font = tree.resolveFontCache();
+      final GuiFontResolver.FontCache font = tree.resolveFontCache();
       int h = font.fontHeight + ClassicTheme.TREELIST_COLUMN_CAPTION_Y_PADDING * 2;
       CoordinatesConversion cc = screen().coordinates();
       return cc.heightFromNative(h);
@@ -213,26 +213,4 @@
          add(ci);
       }
    }
-
-   /**
-    * Finds a caption item widget from its backing model instance.
-    *
-    * @param   column
-    *          Cached column.
-    *
-    * @return  {@linkplain TreeListCaptionItem} instance or {@code null}.
-    */
-   private TreeListCaptionItem findCaptionItem(TreeListGuiImpl.ColumnCache column)
-   {
-      for (Widget<?> w : widgets())
-      {
-         TreeListCaptionItem ci = (TreeListCaptionItem) w;
-         if (Objects.equals(ci.column, column))
-         {
-            return ci;
-         }
-      }
-
-      return null;
-   }
 }

=== modified file 'src/com/goldencode/p2j/ui/client/gui/TreeListCaptionItem.java'
--- src/com/goldencode/p2j/ui/client/gui/TreeListCaptionItem.java	2021-06-02 15:42:33 +0000
+++ src/com/goldencode/p2j/ui/client/gui/TreeListCaptionItem.java	2021-06-17 21:43:40 +0000
@@ -101,7 +101,7 @@
    private static final int MIN_WIDTH = 20;
 
    /** Owning tree */
-   private final TreeListGuiImpl<?> tree;
+   private final TreeListGuiImpl tree;
 
    /** GUI driver */
    private final GuiDriver<?,?> gd;
@@ -145,17 +145,17 @@
     * @param   column
     *          Cached column info.
     */
-   public TreeListCaptionItem(final TreeListGuiImpl<?> tree, final ColumnCache column)
+   public TreeListCaptionItem(final TreeListGuiImpl tree, final ColumnCache column)
    {      
       this.id = WidgetId.nextID();
       this.tree = tree;
-      TreeConfig<?> c = tree.config();
+      final TreeConfig c = tree.config();
 
       // initialize screen driver
       gd = (GuiDriver<?,?>) OutputManager.getDriver();
 
       // color initializing
-      int bgColor = isThreeD() && !c.enabled ? SysColor.COLOR_3DFACE : SysColor.COLOR_WINDOW;
+      final int bgColor = isThreeD() && !c.enabled ? SysColor.COLOR_3DFACE : SysColor.COLOR_WINDOW;
       gc = new GuiColorResolver(gd, c, bgColor, SysColor.COLOR_BTNTEXT);
 
       // at this point classic theme is expected
@@ -163,8 +163,7 @@
 
       this.column = column;
 
-      WidgetRegistry<GuiOutputManager> registry = screen().getRegistry();
-      registry.addWidget(this);
+      screen().getRegistry().addWidget(this);
 
       tree.LOGGER.log(Level.FINE, "TreeListCaptionItem created: " + this);
    }
@@ -203,7 +202,7 @@
       gc.colors();
 
       gd.draw(p, clip, () -> {
-         final boolean isLast = tree.config.isLastVisible(column.column);
+         final boolean isLast = ((TreeListConfig)tree.config).isLastVisible(column.column);
          theme.drawTreeListCaptionItem(gd, gc, tree.resolveFontCache(), dim.height, column,
                   this != ghostCaptionItem, isLast);
       });
@@ -319,7 +318,7 @@
          return;
       }
       
-      final TreeListConfig<?> c = tree.config();
+      final TreeConfig c = tree.config();
 
       // column resizing
       if (atResizeEdge && pushLocation != null)
@@ -351,7 +350,7 @@
       else if (c.sorted)
       {
          final int columnIndex = column.index;
-         c.triggerColumn = columnIndex;
+         ((TreeListConfig)c).triggerColumn = columnIndex;
 
          ThinClient.TriggerResult res = ThinClient.getInstance().invokeTriggers(this,
                   Keyboard.SE_COLUMN_SORTING, false);

=== modified file 'src/com/goldencode/p2j/ui/client/gui/TreeListGuiImpl.java'
--- src/com/goldencode/p2j/ui/client/gui/TreeListGuiImpl.java	2021-06-10 11:50:04 +0000
+++ src/com/goldencode/p2j/ui/client/gui/TreeListGuiImpl.java	2021-06-18 15:47:47 +0000
@@ -95,27 +95,27 @@
  * @param <N> 
  *        Node type
  */
-public class TreeListGuiImpl<N extends TreeListNodeEntry>
-extends TreeGuiImpl<TreeListConfig<N>, TreeListBodyGuiImpl<N>, N>
+public class TreeListGuiImpl
+extends TreeGuiImpl
 {
    /** Caption */
-   TreeListCaption caption;
+   final TreeListCaption caption = new TreeListCaption(this);
 
    /** Cached column definitions */
-   private final List<TreeListGuiImpl.ColumnCache> columns = new ArrayList<>();
+   public final List<TreeListGuiImpl.ColumnCache> columns = new ArrayList<>();
 
    /** Logger used to log all this treelist events */
    Logger LOGGER = Logger.getLogger(TreeListGuiImpl.class.getName());
    
    /**
-    * Construtor.
+    * Constructor.
     *
     * @param   id
     *          Widget id.
     * @param   cfg
     *          Config instance.
     */
-   public TreeListGuiImpl(WidgetId id, TreeListConfig<N> cfg)
+   public TreeListGuiImpl(final WidgetId id, final TreeListConfig cfg)
    {
       super(id, cfg);
    }
@@ -132,13 +132,12 @@
     *          The config used to initialize this widget.
     */
    @Override
-   public void initialize(WidgetId id, TreeListConfig<N> cfg)
+   public void initialize(WidgetId id, TreeConfig cfg)
    {
       super.initialize(id, cfg);
 
       setLayout(new StackLayout<>(StackLayout.Direction.VERTICAL));
-      caption = new TreeListCaption(this);
-      caption.setVisible(config.showHeader);
+      caption.setVisible(((TreeListConfig)config).showHeader);
       add(0, caption);
 
       doLayout();
@@ -161,13 +160,14 @@
     *          Config reference capturing the config state before the owner's config was modified.
     */
    @Override
-   public void afterConfigUpdate(TreeListConfig<N> beforeUpdate)
+   public void afterConfigUpdate(TreeConfig beforeUpdate)
    {
       super.afterConfigUpdate(beforeUpdate);
 
-      if (caption.isVisible() != config.showHeader)
+      final boolean showHeader = ((TreeListConfig)config).showHeader;
+      if (caption.isVisible() != showHeader)
       {
-         caption.setVisible(config.showHeader);
+         caption.setVisible(showHeader);
       }
 
       doLayout();
@@ -177,11 +177,19 @@
     * Lay out container.
     */
    @Override
-   public void doLayout()
+   public final void doLayout()
    {
-      super.doLayout();
-      body.calcSize();  
-      scrollpane.setHeight(height() - getCaptionHeight());
+      /** Place the scrollable container with the body below the caption, so it
+       * fills all remaining space */
+      final double width = width();
+      final double height = height();
+      caption.setLocation(0, 0);
+      final double captionHeight = caption.height();
+      caption.setSize(width, captionHeight);
+      
+      scrollableContainer.setLocation(0, captionHeight);
+      scrollableContainer.setSize(width, height - captionHeight);
+      scrollableContainer.doLayout();
    }
 
    /**
@@ -189,10 +197,9 @@
     *
     * @return  see above.
     */
-   public int getFirstVisibleColumnIndex()
+   public final int getFirstVisibleColumnIndex()
    {
-      ColumnCache col = getColumns().get(0);
-      return col.index;
+      return getColumns().get(0).index;
    }
 
    /**
@@ -201,9 +208,9 @@
     * @return  valid body instance.
     */
    @Override
-   protected TreeListBodyGuiImpl<N> createBody()
+   protected TreeBodyGuiImpl createBody()
    {
-      return new TreeListBodyGuiImpl<N>(this);
+      return new TreeListBodyGuiImpl(this);
    }
 
    /**
@@ -215,7 +222,7 @@
     * @return  the node text.
     */
    @Override
-   protected String getNodeText(final TreeNode<N> node)
+   protected String getNodeText(final TreeNode node)
    {
       final List<ColumnCache> cols = columns;
       assert !cols.isEmpty();
@@ -225,13 +232,7 @@
          return "";
       }
       
-      final List<Object> values = node.value.cellValues;
-      if (position >= values.size() )
-      {
-         return "";
-      }
-      
-      final Object o = values.get(position);
+      final Object o = node.value.getValue(position);
       return o == null ? "" : o.toString();
    }
 
@@ -242,9 +243,9 @@
     *          List of nodes.
     */
    @Override
-   protected void sortNodes(final List<? extends N> nodes)
+   protected void sortNodes(final List<TreeNodeEntry> nodes)
    {
-      final List<Column> ccolumns = config.columns;
+      final List<Column> ccolumns = ((TreeListConfig) config()).columns;
       for (int i = ccolumns.size() - 1; i >= 0; i--)
       {
          final Column column = ccolumns.get(i);
@@ -256,9 +257,7 @@
 
             final Comparator<String> comparator = desc ? Comparator.reverseOrder()
                      : Comparator.naturalOrder();
-            nodes.sort(Comparator.comparing(node -> index < node.cellValues.size()
-                     ? node.cellValues.get(index).toString()
-                     : "", comparator));
+            nodes.sort(Comparator.comparing(node -> node.getValue(index).toString(), comparator));
             return;
          }
       }
@@ -268,7 +267,7 @@
     * Builds tree model from the config's flat list.
     */
    @Override
-   protected void buildTreeModel()
+   protected final void buildTreeModel()
    {
       /**
        * Transform config.columns to this.columns:
@@ -276,8 +275,7 @@
        * <li>sort by column positions
        */
       gd.withSelectedWindow(WindowManager.resolveWindow(this).getId().asInt(), () -> {
-         final TreeListConfig<N> cfg = config();
-         final List<Column> cfgColumns = cfg.columns;
+         final List<Column> cfgColumns = ((TreeListConfig) config()).columns;
          final Column[] visibleColumns = Iterables
                   .toArray(Iterables.filter(cfgColumns, c -> c.visible), Column.class);
 
@@ -466,8 +464,8 @@
          return;
       }
 
-      final TreeListConfig<?> config = config();
-      final List<Column> configColumns = config.columns;
+      final TreeConfig config = config();
+      final List<Column> configColumns = ((TreeListConfig)config).columns;
       final int configColumnsSize = configColumns.size();
       if (newSourceIndex > sourceIndex)
       {

=== modified file 'src/com/goldencode/p2j/ui/client/gui/TreeViewBodyGuiImpl.java'
--- src/com/goldencode/p2j/ui/client/gui/TreeViewBodyGuiImpl.java	2021-05-28 17:57:56 +0000
+++ src/com/goldencode/p2j/ui/client/gui/TreeViewBodyGuiImpl.java	2021-06-17 16:19:20 +0000
@@ -89,7 +89,7 @@
  * Implementation of the tree view body, this is the part of the widget where the user input-output happens.
  */
 public class TreeViewBodyGuiImpl
-extends TreeBodyGuiImpl<TreeViewConfig, TreeViewGuiImpl, TreeViewNodeEntry>
+extends TreeBodyGuiImpl
 {
    /**
     * Constructor.
@@ -97,7 +97,7 @@
     * @param   treeView
     *          The owning tree view.
     */
-   public TreeViewBodyGuiImpl(TreeViewGuiImpl treeView)
+   public TreeViewBodyGuiImpl(TreeGuiImpl treeView)
    {
       super(treeView);
    }
@@ -140,27 +140,28 @@
       final NativeDimension dim = physicalDimension();
       final NativeRectangle clip = clipRectangle(p, dim);
       
-      TreeNode<TreeViewNodeEntry> highlightNode = tree.getDropHighlightNode();
+      TreeNode highlightNode = tree.getDropHighlightNode();
       
       if (highlightNode == null)
       {
          highlightNode = tree.getCurrentNode();
       }
       
-      final TreeNode<TreeViewNodeEntry> dropHighlightOrSelected = highlightNode;
+      final TreeNode dropHighlightOrSelected = highlightNode;
+      final TreeNode topNode = tree.findNode(tree.config.topNode);
       
       // clippable drawing start
       gd.draw(p, clip, () -> theme.drawTreeViewBody(gd,
                                                     gc,
                                                     font,
                                                     tree,
-                                                    tree.rootNode.nodes,
+                                                    topNode,
                                                     dropHighlightOrSelected,
                                                     0,
                                                     dim.width,
+                                                    dim.height,
                                                     selectionBack,
-                                                    selectionFore,
-                                                    disabledFore));
+                                                    selectionFore, disabledFore));
       
       if (tree.isEditorActivated())
       {
@@ -181,27 +182,28 @@
     * @return  the calculated width.
     */
    @Override
-   protected int calcMaxNodesWidth(List<TreeGuiImpl.TreeNode<TreeViewNodeEntry>> nodes, int maxWidth, int level)
+   protected int calcMaxNodesWidth(final List<TreeNode> nodes, final int maxWidth, final int level)
    {
-      for (TreeGuiImpl.TreeNode<TreeViewNodeEntry> node : nodes)
+      int result = maxWidth;
+      for (TreeNode node : nodes)
       {
          int width = calcNodeWidth(node, level);
-         if (width > maxWidth)
+         if (width > result)
          {
-            maxWidth = width;
+            result = width;
          }
 
          if (node.value.expanded && node.nodes != null)
          {
-            width = calcMaxNodesWidth(node.nodes, maxWidth, level + 1);
-            if (width > maxWidth)
+            width = calcMaxNodesWidth(node.nodes, result, level + 1);
+            if (width > result)
             {
-               maxWidth = width;
+               result = width;
             }
          }
       }
 
-      return maxWidth;
+      return result;
    }
 
    /**
@@ -214,7 +216,7 @@
     *
     * @return  the calculated width.
     */
-   private int calcNodeWidth(TreeGuiImpl.TreeNode<?> node, int level)
+   private int calcNodeWidth(TreeNode node, int level)
    {
       ImageWrapper<?> image = tree.getNodeImage(node, true);
       
@@ -243,5 +245,31 @@
    {
       return true;
    }
+
+   /**
+    * Update the vertical scrolling model based on this object geometry and contents.
+    * 
+    * @return {@code true} if model was changed as the result of the call.
+    */
+   @Override
+   public boolean updateVScrollModel()
+   {
+      // FIXME
+      assert false;
+      return false;
+   }
+
+   /**
+    * Update the horizontal scrolling model based on this object geometry and contents.
+    * 
+    * @return {@code true} if model was changed as the result of the call.
+    */
+   @Override
+   public boolean updateHScrollModel()
+   {
+      // FIXME
+      assert false;
+      return false;
+   }
 }
 

=== modified file 'src/com/goldencode/p2j/ui/client/gui/TreeViewGuiImpl.java'
--- src/com/goldencode/p2j/ui/client/gui/TreeViewGuiImpl.java	2021-05-25 15:17:26 +0000
+++ src/com/goldencode/p2j/ui/client/gui/TreeViewGuiImpl.java	2021-06-17 16:19:49 +0000
@@ -86,7 +86,7 @@
  * TREEVIEW widget client-side implementation.
  */
 public class TreeViewGuiImpl
-extends TreeGuiImpl<TreeViewConfig, TreeViewBodyGuiImpl, TreeViewNodeEntry>
+extends TreeGuiImpl
 {
    
    /**
@@ -120,9 +120,9 @@
     *          List of nodes.
     */
    @Override
-   protected void sortNodes(final List<? extends TreeViewNodeEntry> nodes)
+   protected void sortNodes(final List<TreeNodeEntry> nodes)
    {
-      nodes.sort(Comparator.comparing(node -> node.text));
+      nodes.sort(Comparator.comparing(node -> node.getValue().toString()));
    }
 
    /**
@@ -134,9 +134,9 @@
     * @return  the node text.
     */
    @Override
-   protected String getNodeText(TreeNode<TreeViewNodeEntry> node)
+   protected String getNodeText(final TreeNode node)
    {
-      return ((TreeViewNodeEntry) node.value).text;
+      return node.value.getValue().toString();
    }
 
    /**

=== modified file 'src/com/goldencode/p2j/ui/client/gui/WindowGuiImpl.java'
--- src/com/goldencode/p2j/ui/client/gui/WindowGuiImpl.java	2021-02-24 23:47:59 +0000
+++ src/com/goldencode/p2j/ui/client/gui/WindowGuiImpl.java	2021-06-17 16:20:05 +0000
@@ -256,7 +256,6 @@
 import java.util.*;
 import java.util.function.*;
 
-import com.goldencode.p2j.oo.core.collections.*;
 import com.goldencode.p2j.ui.*;
 import com.goldencode.p2j.ui.chui.*;
 import com.goldencode.p2j.ui.client.*;

=== modified file 'src/com/goldencode/p2j/ui/client/gui/driver/swing/SwingEmulatedWindow.java'
--- src/com/goldencode/p2j/ui/client/gui/driver/swing/SwingEmulatedWindow.java	2021-05-21 23:19:22 +0000
+++ src/com/goldencode/p2j/ui/client/gui/driver/swing/SwingEmulatedWindow.java	2021-06-10 11:58:36 +0000
@@ -502,6 +502,9 @@
       // register listeners for OS window events
       window.addWindowListener(new WindowAdapter()
       {
+         /** Serial Version ID */
+         private static final long serialVersionUID = -640373783569803478L;
+
          @Override
          public void windowIconified(WindowEvent e)
          {
@@ -591,6 +594,9 @@
       
       window.addComponentListener(new ComponentAdapter()
       {
+         /** Serial Version ID */
+         private static final long serialVersionUID = -640373783569803478L;
+
          @Override
          public void componentShown(ComponentEvent e)
          {

=== modified file 'src/com/goldencode/p2j/ui/client/gui/theme/ClassicTheme.java'
--- src/com/goldencode/p2j/ui/client/gui/theme/ClassicTheme.java	2021-06-10 11:50:04 +0000
+++ src/com/goldencode/p2j/ui/client/gui/theme/ClassicTheme.java	2021-06-18 18:47:18 +0000
@@ -194,6 +194,8 @@
 import com.goldencode.p2j.ui.client.gui.*;
 import com.goldencode.p2j.ui.client.gui.SliderGuiImpl.*;
 import com.goldencode.p2j.ui.client.gui.TabSetGuiImpl.*;
+import com.goldencode.p2j.ui.client.gui.TreeGuiImpl.TreeNode;
+import com.goldencode.p2j.ui.client.gui.TreeListGuiImpl.*;
 import com.goldencode.p2j.ui.client.gui.driver.*;
 import com.goldencode.p2j.ui.client.gui.driver.LightweightTextWidget.*;
 import com.goldencode.p2j.ui.client.gui.theme.Theme.TextLookAndFeel.*;
@@ -215,7 +217,7 @@
    public static final int TREEVIEW_INNER_PADDING = 2;
 
    /** Treeview expand icon horizontal offset */
-   public static final int TREEVIEW_EXPAND_ICON_OFFSET = 7;
+   public static final int TREEVIEW_EXPAND_ICON_OFFSET = 1;
 
    /** Treeview expand icon size */
    public static final int TREEVIEW_EXPAND_ICON_SIZE = 9;
@@ -344,7 +346,7 @@
    protected static final int ROW_TOGGLE_SIZE = 13;
 
    /** Treeview node level offset */
-   private static final int TREEVIEW_NODE_LEVEL_OFFSET = 26;
+   private static final int TREEVIEW_NODE_LEVEL_OFFSET = 16;
 
    /** Color of the treeview expand icon */
    private static final ColorRgb TREEVIEW_EXPAND_ICON_COLOR = new ColorRgb(0x000000);
@@ -2245,6 +2247,7 @@
     * @param  chkMarkColor
     *         ColorRgb - RGB color for checkmark.
     */
+   @SuppressWarnings("static-method")
    private void drawCheckmark(GuiDriver gd, NativePoint p, ColorRgb chkMarkColor)
    {
       gd.setColor(chkMarkColor);
@@ -2459,41 +2462,43 @@
     *          If the mouse is hovering over the button.
     */
    @Override
-   public void drawScrollBarButton(GuiDriver gd,
-                                   GuiColorResolver gc,
-                                   int x,
-                                   int y,
-                                   int width,
-                                   int height,
-                                   ScrollBar.Position pos,
-                                   boolean horizontal,
-                                   boolean enabled,
-                                   boolean pressed,
-                                   boolean highlighted)
+   public void drawScrollBarButton(final GuiDriver gd,
+                                   final GuiColorResolver gc,
+                                   final int x,
+                                   final int y,
+                                   final int width,
+                                   final int height,
+                                   final ScrollBar.Position pos,
+                                   final boolean horizontal,
+                                   final boolean enabled,
+                                   final boolean pressed,
+                                   final boolean highlighted)
    {
+      int xi = x;
+      int yi = y;
       gd.setColor(gc.bgColor);
-      gd.fillRect(x, y, width, height);
+      gd.fillRect(xi, yi, width, height);
       
       // 3D border
       if (!pressed)
       {
          // top-left part
          gd.setColor(gd.getSysColor(SysColor.COLOR_WINDOW));
-         gd.drawLine(x + 1, y + 1, x + 1,         y + height - 3);
-         gd.drawLine(x + 2, y + 1, x + width - 3, y + 1);
+         gd.drawLine(xi + 1, yi + 1, xi + 1,         yi + height - 3);
+         gd.drawLine(xi + 2, yi + 1, xi + width - 3, yi + 1);
          // bottom-right part
          gd.setColor(gd.getSysColor(SysColor.COLOR_3DDKSHADOW));
-         gd.drawLine(x,             y + height - 1, x + width - 1, y + height - 1);
-         gd.drawLine(x + width - 1, y,              x + width - 1, y + height - 2);
+         gd.drawLine(xi,             yi + height - 1, xi + width - 1, yi + height - 1);
+         gd.drawLine(xi + width - 1, yi,              xi + width - 1, yi + height - 2);
          // shadow corner
          gd.setColor(gd.getSysColor(SysColor.COLOR_3DSHADOW));
-         gd.drawLine(x + 1,         y + height - 2, x + width - 2, y + height - 2);
-         gd.drawLine(x + width - 2, y + 1,          x + width - 2, y + height - 3);
+         gd.drawLine(xi + 1,         yi + height - 2, xi + width - 2, yi + height - 2);
+         gd.drawLine(xi + width - 2, yi + 1,          xi + width - 2, yi + height - 3);
       }
       else
       {
          gd.setColor(gd.getSysColor(SysColor.COLOR_3DSHADOW));
-         gd.drawRect(x, y, width - 1, height - 1);
+         gd.drawRect(xi, yi, width - 1, height - 1);
       }
    
       // draw arrow images loaded before
@@ -2529,13 +2534,13 @@
          // calc size and centered location
          w = img.getWidth();
          h = img.getHeight();
-         x += (width - w) / 2;
-         y += (height - h) / 2;
+         xi += (width - w) / 2;
+         yi += (height - h) / 2;
       
          // for enabled buttons need to consider Up/Down state difference
          gd.drawImage(img,
-               !pressed || !enabled ? x : x + 1,
-               !pressed || !enabled ? y : y + 1,
+               !pressed || !enabled ? xi : xi + 1,
+               !pressed || !enabled ? yi : yi + 1,
                w, h, 0, 0, false, false, false, false);
       }
    }
@@ -2657,6 +2662,7 @@
     * @param   y2
     *          The bottom extremity of the caret.
     */
+   @SuppressWarnings("static-method")
    protected void drawCaret(GuiDriver gd, int x1, int y1, int x2, int y2)
    {
       // the caret color to be XOR-ed with current content
@@ -3872,6 +3878,7 @@
     * @param   gd
     *          the active {@code GuiDriver} used to draw the browse widget.
     */
+   @SuppressWarnings("static-method")
    protected void setCompositeXorColor(GuiDriver gd)
    {
       gd.setColor(BROWSE_ROW_XOR_COLOR);
@@ -4049,8 +4056,8 @@
                                     GuiDriver gd,
                                     GuiColorResolver gc,
                                     GuiFontResolver gf,
-                                    int x,
-                                    int y,
+                                    final int x,
+                                    final int y,
                                     int width,
                                     int height)
    {
@@ -4065,14 +4072,13 @@
       // calculate text position
       FontDetails font = gf.font().font;
       int textWidth = browse.getTextWidth(title, font);
-      x += (width - textWidth) / 2;
       
       // draw title string
       gd.setColor(focused ? gc.titleFgColor : titleInactiveFgColor);
       gd.setGuiFont(font);
       gd.drawStringCentered(title, 
-                            x, 
-                            browse.getTitleHeight() / 2 + BrowseGuiImpl.BORDER_THICKNESS);
+               x + (width - textWidth) / 2, 
+               browse.getTitleHeight() / 2 + BrowseGuiImpl.BORDER_THICKNESS);
    }
    
    /**
@@ -4372,6 +4378,7 @@
     *
     * @return row marker image for a row.
     */
+   @SuppressWarnings("static-method")
    protected ImageWrapper getBrowseMarkerArrow(boolean selected)
    {
       final int ROW_MARKER_IMAGE_SIZE = 16;
@@ -4418,6 +4425,7 @@
     *
     * @return the image of a non-edited in-browse toggle-box.
     */
+   @SuppressWarnings("static-method")
    protected ImageWrapper getRowToggleImage(boolean checked)
    {
       return VectorGraphicsHelper.getScalableImage(checked ? "row-toggle-on" : "row-toggle-off",
@@ -4434,6 +4442,7 @@
     *          The color for unselected cells. If {@code null} then the unselected cells' 
     *          background is not drawn. 
     */
+   @SuppressWarnings("static-method")
    protected void setBrowseCellBackgroundSelectionColor(GuiDriver gd, ColorRgb unselColor)
    {
       gd.setColor(ColorManager.getSysColor(SysColor.COLOR_ACTIVECAPTION));
@@ -4874,6 +4883,7 @@
     *
     * @return clip point for the sort arrow inside the column header.
     */
+   @SuppressWarnings("static-method")
    protected NativePoint getSortArrowClipPoint(BrowseColumnGuiImpl column,
                                                NativePoint textClipPoint,
                                                NativeRectangle textClipRectangle)
@@ -4916,6 +4926,7 @@
     * @param   y
     *          Starting Y coord of the text.
     */
+   @SuppressWarnings("static-method")
    protected void drawBrowseCellLabel(BrowseColumnGuiImpl column,
                                       GuiDriver gd,
                                       String text,
@@ -5032,12 +5043,11 @@
 
       // calculate text position
       int textWidth = frame.getTextWidthNative(title, gf.resolveFontNum());
-      x += (width - textWidth) / 2 - 1;
       
       // draw title string
       gd.setColor(gc.titleFgColor);
       gd.setGuiFont(gf.font().font);
-      gd.drawStringCentered(title, x, height / 2);
+      gd.drawStringCentered(title, x + (width - textWidth) / 2 - 1, height / 2);
    }
 
    /**
@@ -5343,8 +5353,8 @@
                                  GuiDriver gd,
                                  GuiFontResolver gf,
                                  GuiColorResolver gc,
-                                 int x,
-                                 int y,
+                                 final int x,
+                                 final int y,
                                  int width,
                                  int height,
                                  NativePoint selStart,
@@ -5352,6 +5362,7 @@
                                  int dx,
                                  int dy)
    {
+      int yi = y;
       gd.setGuiFont(gf.font().font);
       
       int fontDescent = 2; // TODO: use font.descent instead of constant 2
@@ -5374,7 +5385,7 @@
             gd.drawTextSelection(textLine,
                                  textLookAndFeel,
                                  x - dx,
-                                 y,
+                                 yi,
                                  fontHeight,
                                  fontDescent);
          }
@@ -5385,10 +5396,10 @@
             gd.setColor(textLookAndFeel.getTextColor());
             
             // adjust for scrollX
-            gd.drawString(txt, x - dx, y + fontHeight - fontDescent);
+            gd.drawString(txt, x - dx, yi + fontHeight - fontDescent);
          }
          
-         y += fontHeight;
+         yi += fontHeight;
       }
    }
    
@@ -5636,6 +5647,7 @@
     * @return <code>true</code> if the upper part of the column's right separator should NOT be
     *         drawn because of the quirk described above.
     */
+   @SuppressWarnings("static-method")
    protected boolean isBrowseLabelBorderDrawQuirk(BrowseColumnGuiImpl column)
    {
       return column.isLastLockedColumn() &&
@@ -6115,10 +6127,12 @@
    public void drawSliderTicMarks(SliderGuiImpl slider,
                                    GuiDriver gd,
                                    GuiColorResolver gc,
-                                   int x,
-                                   int y,
+                                   final int x,
+                                   final int y,
                                    int length)
    {
+      int xi = x;
+      int yi = y;
       // get slider config
       SliderConfig config = slider.config();
       TicMarks tmMode = slider.getTicmarkMode();
@@ -6140,10 +6154,10 @@
          // tics above ruler 
          if (tmMode == TicMarks.TOP || tmMode == TicMarks.BOTH)
          {
-            y += tmMode == TicMarks.TOP ? 1 : 0;
+            yi += tmMode == TicMarks.TOP ? 1 : 0;
             // extra length marks on edges
-            gd.drawLine(x, y - shift, x, y - shift - TM_LENGTH - 1);
-            gd.drawLine(x + length, y - shift, x + length, y - shift - TM_LENGTH - 1);
+            gd.drawLine(xi, yi - shift, xi, yi - shift - TM_LENGTH - 1);
+            gd.drawLine(xi + length, yi - shift, xi + length, yi - shift - TM_LENGTH - 1);
             
             // regular tics between edges, when frequency == 0 there must be no tics between edges
             if (config.frequency > 0)
@@ -6151,8 +6165,8 @@
                for (int i = config.minValue + config.frequency;
                     i < config.maxValue; i += config.frequency)
                {
-                  int xt = slider.indexToCoord(x, length, true, i);
-                  gd.drawLine(xt, y - shift, xt, y - shift - TM_LENGTH);
+                  int xt = slider.indexToCoord(xi, length, true, i);
+                  gd.drawLine(xt, yi - shift, xt, yi - shift - TM_LENGTH);
                }
             }
             // BOTH mode need to change the shift
@@ -6164,11 +6178,11 @@
          // tics below ruler
          if (tmMode == TicMarks.BOTTOM || tmMode == TicMarks.BOTH)
          {
-            y -= tmMode == TicMarks.BOTTOM ? 1 : 0;
+            yi -= tmMode == TicMarks.BOTTOM ? 1 : 0;
             
             // extra length marks on edges
-            gd.drawLine(x, y + shift, x, y + shift + TM_LENGTH + 1);
-            gd.drawLine(x + length, y + shift, x + length, y + shift + TM_LENGTH + 1);
+            gd.drawLine(xi, yi + shift, xi, yi + shift + TM_LENGTH + 1);
+            gd.drawLine(xi + length, yi + shift, xi + length, yi + shift + TM_LENGTH + 1);
             
             // regular tics between edges, when frequency == 0 there must be no tics between edges
             if (config.frequency > 0)
@@ -6176,8 +6190,8 @@
                for (int i = config.minValue + config.frequency;
                     i < config.maxValue; i += config.frequency)
                {
-                  int xt = slider.indexToCoord(x, length, true, i);
-                  gd.drawLine(xt, y + shift, xt, y + shift + TM_LENGTH);
+                  int xt = slider.indexToCoord(xi, length, true, i);
+                  gd.drawLine(xt, yi + shift, xt, yi + shift + TM_LENGTH);
                }
             }
          }
@@ -6187,11 +6201,11 @@
          // tics left to the ruler
          if (tmMode == TicMarks.LEFT || tmMode == TicMarks.BOTH)
          {
-            x += tmMode == TicMarks.LEFT ? 1 : 0;
+            xi += tmMode == TicMarks.LEFT ? 1 : 0;
             
             // extra length marks on edges
-            gd.drawLine(x - shift, y, x - shift - TM_LENGTH - 1, y);
-            gd.drawLine(x - shift, y + length, x - shift - TM_LENGTH - 1, y + length);
+            gd.drawLine(xi - shift, yi, xi - shift - TM_LENGTH - 1, yi);
+            gd.drawLine(xi - shift, yi + length, xi - shift - TM_LENGTH - 1, yi + length);
             
             // regular tics between edges, when frequency == 0 there must be no tics between edges
             if (config.frequency > 0)
@@ -6199,8 +6213,8 @@
                for (int i = config.minValue + config.frequency;
                     i < config.maxValue; i += config.frequency)
                {
-                  int yt = slider.indexToCoord(y, length, true, i);
-                  gd.drawLine(x - shift, yt, x - shift - TM_LENGTH, yt);
+                  int yt = slider.indexToCoord(yi, length, true, i);
+                  gd.drawLine(xi - shift, yt, xi - shift - TM_LENGTH, yt);
                }
             }
             // BOTH mode need to change the shift
@@ -6213,11 +6227,11 @@
          // tics right ruler
          if (tmMode == TicMarks.RIGHT || tmMode == TicMarks.BOTH)
          {
-            x -= tmMode == TicMarks.RIGHT ? 1 : 0;
+            xi -= tmMode == TicMarks.RIGHT ? 1 : 0;
             
             // extra length marks on edges
-            gd.drawLine(x + shift, y, x + shift + TM_LENGTH + 1, y);
-            gd.drawLine(x + shift, y + length, x + shift + TM_LENGTH + 1, y + length);
+            gd.drawLine(xi + shift, yi, xi + shift + TM_LENGTH + 1, yi);
+            gd.drawLine(xi + shift, yi + length, xi + shift + TM_LENGTH + 1, yi + length);
             
             // regular tics between edges, when frequency == 0 there must be no tics between edges
             if (config.frequency > 0)
@@ -6225,8 +6239,8 @@
                for (int i = config.minValue + config.frequency;
                     i < config.maxValue; i += config.frequency)
                {
-                  int yt = slider.indexToCoord(y, length, true, i);
-                  gd.drawLine(x + shift, yt, x + shift + TM_LENGTH, yt);
+                  int yt = slider.indexToCoord(yi, length, true, i);
+                  gd.drawLine(xi + shift, yt, xi + shift + TM_LENGTH, yt);
                }
             }
          }
@@ -6241,6 +6255,7 @@
     *
     * @return  the color used for drawing the tic-marks. 
     */
+   @SuppressWarnings("static-method")
    protected ColorRgb getSliderTicmarkColor(GuiColorResolver gc)
    {
       return gc.fgColor;
@@ -6442,7 +6457,7 @@
     * @return   The tree node label's horizontal offset
     */
    @Override
-   public int getTreeViewNodeContentHorizontalOffset(int level)
+   public int getTreeViewNodeContentHorizontalOffset(final int level)
    {
       return TREEVIEW_INNER_PADDING + getTreeViewNodeHorizontalOffset(level);
    }
@@ -6556,6 +6571,27 @@
    }
 
    /**
+    * Draw a dotted vertical line with current color.
+    * 
+    * @param gd
+    *        the gui driver
+    * @param x
+    *        the x coordinate of both line ends
+    * @param top
+    *        the y coordinate of the line start
+    * @param bottom
+    *        the y coordinate of the line end, must be greater or equal the top argument
+    */
+   private static void drawDottedVerticalLine(final GuiDriver gd, final int x, final int top,
+            final int bottom)
+   {
+      for (int y = top; y < bottom; y += 2)
+      {
+         gd.drawLine(x, y, x, y);
+      }
+   }
+   
+   /**
     * Draws treeview body.
     *
     * @param   gd
@@ -6566,14 +6602,16 @@
     *          Text font.
     * @param   tree
     *          Tree widget, the owner of the tree body.
-    * @param   nodes
-    *          Nodes to draw.
+    * @param   topNode
+    *          the first node to draw, may by {@code null} for an empty tree.
     * @param   currentNode
     *          Current node.
     * @param   valueIndex
     *          The index of the value to be displayed. For TREEVIEW widget this is always 0.
     * @param   width
-    *          Treeview body dimension.
+    *          Treeview body width.
+    * @param   height
+    *          Treeview body height.
     * @param   selectionBack
     *          Selection back color.
     * @param   selectionFore
@@ -6581,214 +6619,420 @@
     * @param   disabledFore
     *          Disabled widget foreground color.
     */
-   public final <N extends TreeNodeEntry> void drawTreeViewBody(final GuiDriver gd,
-                                                                final GuiColorResolver gc,
-                                                                final GuiFontResolver.FontCache font,
-                                                                final TreeGuiImpl tree,
-                                                                final List<TreeGuiImpl.TreeNode<N>> nodes,
-                                                                final TreeGuiImpl.TreeNode<N> currentNode,
-                                                                final int valueIndex,
-                                                                final int width,
-                                                                final ColorRgb selectionBack,
-                                                                final ColorRgb selectionFore,
-                                                                final ColorRgb disabledFore)
+   public final void drawTreeViewBody(final GuiDriver gd,
+                                      final GuiColorResolver gc,
+                                      final GuiFontResolver.FontCache font,
+                                      final TreeGuiImpl tree,
+                                      final TreeNode topNode,
+                                      final TreeNode currentNode,
+                                      final int valueIndex,
+                                      final int width,
+                                      final int height,
+                                      final ColorRgb selectionBack,
+                                      final ColorRgb selectionFore,
+                                      final ColorRgb disabledFore)
    {
       // fill the background
-      // 
       // FIXME: filling background does not change anything at the moment, so it is commented for now
       // gd.setColor(gc.bgColor);
       // gd.fillRect(0, 0, bodyDim.width, bodyDim.height);
 
-      if (nodes != null && !nodes.isEmpty())
-      {
-         gd.setGuiFont(font.font);
-         gd.setFontStyle(font.font.style);
-         final int rowHeight = getTreeViewNodeHeight(font);
-
-         drawTreeViewNodes(gd, gc, tree, nodes, currentNode, valueIndex, 0, font, rowHeight,
-                  width, 0, tree.isEnabled(), selectionBack, selectionFore, disabledFore);
+      if (topNode == null)
+      {
+         return;
+      }
+      
+      gd.setGuiFont(font.font);
+      gd.setFontStyle(font.font.style);
+      final int rowHeight = getTreeViewNodeHeight(font);
+
+      // Collect visible nodes that fit into the widget height, their row numbers and parents
+      final Map<TreeNode, Integer> node2row = new IdentityHashMap<>();
+      final Set<TreeNode> parents = Collections.newSetFromMap(new IdentityHashMap<>()); 
+      final int nodesFit = (height + rowHeight - 1) / rowHeight;
+      int row = 0; 
+      for (TreeNode node = topNode; node != null && row < nodesFit; node = node.nextVisible())
+      {
+         node2row.put(node, Integer.valueOf(row));
+         parents.add(node.parent);
+         row++; 
+      }
+      
+      // Add ancestors of the top node
+      for (TreeNode p = topNode.parent; p != null; p = p.parent)
+      {
+         parents.add(p);
+      }
+      
+      final int signOff = TREEVIEW_EXPAND_ICON_SIZE / 2;
+
+      // draw vertical lines from parents down to the last children
+      gd.setColor(TREEVIEW_DOTS_COLOR);
+      for (final TreeNode p : parents)
+      {
+         final int level = p.getLevel();
+         final int expandIconLeftX = TREEVIEW_INNER_PADDING + TREEVIEW_EXPAND_ICON_OFFSET
+                  + getTreeViewNodeHorizontalOffset(level);
+         final int verticalDotsX = expandIconLeftX + signOff;
+         final Integer parentRow = node2row.get(p);
+         final int top = parentRow != null ? rowHeight * parentRow.intValue() + rowHeight / 2 : 0;
+
+         final List<TreeNode> children = p.nodes;
+         final TreeNode lastChild = children.get(children.size() - 1);
+
+         // Skip lines completely above the visible area
+         if (lastChild != topNode && lastChild.isDFTBeforeThan(topNode))
+         {
+            continue;
+         }
+         
+         final Integer lastChildRow = node2row.get(lastChild);
+         final int bottom = lastChildRow != null
+                  ? rowHeight * lastChildRow.intValue() + rowHeight / 2
+                  : height;
+         drawDottedVerticalLine(gd, verticalDotsX, top, bottom);
+      }
+      
+      final TreeConfig c = tree.config();
+      final boolean configShowExpandIcon = c.showExpandIcon;
+      final boolean enabled = tree.isEnabled();
+      final int hSignOff = signOff / 2;
+      for (final TreeNode node : node2row.keySet())
+      {
+         final int level = node.getLevel();
+         final int contentX = getTreeViewNodeContentHorizontalOffset(level);
+         final int expandIconLeftX = TREEVIEW_INNER_PADDING + TREEVIEW_EXPAND_ICON_OFFSET
+                  + getTreeViewNodeHorizontalOffset(level - 1);
+
+         int r = node2row.get(node).intValue();
+         final int iconY = rowHeight * r + (rowHeight - TREEVIEW_EXPAND_ICON_SIZE) / 2;
+
+         // horizontal dots from the center of expand icon to the center of node icon.
+         gd.setColor(TREEVIEW_DOTS_COLOR);
+         final int horizontalDotsY = iconY + signOff;
+         final int startX = expandIconLeftX + hSignOff + 2;
+         final int endX = signOff + contentX + 3;
+         for (int x = startX; x < endX; x += 2)
+         {
+            gd.drawLine(x, horizontalDotsY, x, horizontalDotsY);
+         }
+         
+         final TreeNodeEntry nodeValue = node.value;
+         final Object value = nodeValue.getValue(valueIndex);      
+         final String text = value == null ? "" : value.toString();
+         final boolean nodeSelected = node == currentNode;
+         final ImageWrapper img = tree.getNodeImage(node, nodeSelected);
+         final int imageWidth = img != null ? img.getWidth() : 0;         
+         final int textX = getTreeViewNodeLabelHorizontalOffset(level, imageWidth);
+
+         // node icon
+         if (img != null)
+         {
+            final int imageHeight = img.getHeight();
+            final int imageY = getTreeViewNodeImageVerticalOffset(r, rowHeight, imageHeight);
+            gd.drawImage(img, contentX - 2, imageY, imageWidth, imageHeight, 0, 0, false, false,
+                     false, false);
+         }
+
+         // Expand/collapse button
+         if (configShowExpandIcon && nodeValue.hasChildren)
+         {
+            final int expandIconDimension = TREEVIEW_EXPAND_ICON_SIZE - 1;
+
+            // Button background
+            gd.setColor(gc.bgColor);
+            gd.fillRect(expandIconLeftX, iconY, expandIconDimension, expandIconDimension);
+            
+            // Button border
+            gd.setColor(TREEVIEW_DOTS_COLOR);
+            gd.drawRect(expandIconLeftX, iconY, expandIconDimension, expandIconDimension);
+
+            // the "-" sign line
+            gd.setColor(TREEVIEW_EXPAND_ICON_COLOR);
+            gd.drawLine(expandIconLeftX + hSignOff, iconY + signOff,
+                     expandIconLeftX + hSignOff + signOff, iconY + signOff);
+
+            // the line tom make "+" sign
+            if (!node.value.expanded)
+            {
+               gd.drawLine(expandIconLeftX + signOff, iconY + hSignOff, expandIconLeftX + signOff,
+                        iconY + hSignOff + signOff);
+            }
+         }
+
+//         drawTreeListCell(gd,
+//                  font,
+//                  tree,
+//                  cellWidth,
+//                  rowHeight,
+//                  fgColor,
+//                  bgSelColor,
+//                  bgColor,
+//                  Object value,
+//                  align,
+//                  selected,
+//                  final TreeListConfig.Column.DataType dataType);
+         
+ 
+         // node text highlight
+         if (nodeSelected)
+         {
+            gd.setColor(selectionBack);
+            gd.fillRect(textX, rowHeight * r, width, rowHeight);
+         }
+
+         // node text color
+         final ColorRgb textColor;
+         if (enabled && nodeSelected)
+         {
+            textColor = selectionFore;
+         }
+         else if (enabled)
+         {
+            textColor = gc.fgColor;
+         }
+         else
+         {
+            textColor = disabledFore;
+         }
+
+         // node text
+         gd.setColor(textColor);
+         final int textY = getTreeViewNodeVerticalOffset(r, rowHeight, font);
+         gd.drawString(text, textX + 2, textY);
+
       }
    }
 
    /**
-    * Draws a treeview node.
+    * Draws treeview body.
     *
     * @param   gd
     *          GUI driver.
     * @param   gc
     *          Color resolver.
+    * @param   fc
+    *          Text font.
     * @param   tree
     *          Tree widget, the owner of the tree body.
-    * @param   node
-    *          Node to draw.
+    * @param   topNode
+    *          the first node to draw or {@code null} for an empty tree.
     * @param   currentNode
-    *          Current node.
-    * @param   valueIndex
-    *          The index of the value to be displayed. For TREEVIEW widget this is always 0.
-    * @param   rowIdx
-    *          Row index.
-    * @param   parentRowIdx
-    *          Parent row index.
-    * @param   prevExpIcoRowIdx
-    *          Row index of the previous sibling with expand icon.
-    * @param   font
-    *          The font to use.
-    * @param   nodeHeight
-    *          Node height.
-    * @param   rowWidth
-    *          Row width.
-    * @param   level
-    *          Tree level.
-    * @param   enabled
-    *          Widget enabled state.
-    * @param   firstChild
-    *          Whether the node is the first node in its parent collection.
-    * @param   lastChild
-    *          Whether the node is the last node in its parent collection.
-    * @param   showExpandIcon
-    *          Make the expand icon visible or invisible.
+    *          Selected node or {@code null} for an empty tree.
+    * @param   width
+    *          widget width.
+    * @param   height
+    *          widget height.
     * @param   selectionBack
-    *          Selection back color.
+    *          Selection background color.
     * @param   selectionFore
     *          Selection foreground color.
-    * @param   disabledFore
-    *          Disabled widget foreground color.
-    * @param   imageListId
-    *          Widget id of the image list assignd to the tree.
     */
-   private void drawTreeViewNode(final GuiDriver gd,
-                                 final GuiColorResolver gc,
-                                 final TreeGuiImpl tree,
-                                 final TreeGuiImpl.TreeNode node,
-                                 final TreeGuiImpl.TreeNode currentNode,
-                                 final int valueIndex,
-                                 final int rowIdx,
-                                 final int parentRowIdx,
-                                 final int prevExpIcoRowIdx,
-                                 final GuiFontResolver.FontCache font,
-                                 final int nodeHeight,
-                                 final int rowWidth,
-                                 final int level,
-                                 final boolean enabled,
-                                 final boolean firstChild,
-                                 final boolean lastChild,
-                                 final boolean showExpandIcon,
-                                 final ColorRgb selectionBack,
-                                 final ColorRgb selectionFore,
-                                 final ColorRgb disabledFore,
-                                 final int imageListId)
+   public final void drawTreeListBody(final GuiDriver gd,
+                                      final GuiColorResolver gc,
+                                      final GuiFontResolver.FontCache fc,
+                                      final TreeListGuiImpl tree,
+                                      final TreeNode topNode,
+                                      final TreeNode currentNode,
+                                      final int width,
+                                      final int height,
+                                      final ColorRgb selectionBack,
+                                      final ColorRgb selectionFore)
    {
-      final Object value = node.value.getValue(valueIndex);
-      
-      final String text = value == null ? "" : value.toString();
-
-      int contentX = getTreeViewNodeContentHorizontalOffset(level);
-      
-      int textY = getTreeViewNodeVerticalOffset(rowIdx, nodeHeight, font);
-
-      // node image if defined
-      // selected item needs special background and foreground
-      final boolean nodeSelected = node == currentNode;
-      final ImageWrapper img = tree.getNodeImage(node, nodeSelected);
-
-      final int imageWidth;
-      if (img != null)
-      {
-         imageWidth = img.getWidth();      
-         final int imageHeight = img.getHeight();
-         final int imageY = getTreeViewNodeImageVerticalOffset(rowIdx, nodeHeight, imageHeight);         
-         gd.drawImage(img,
-                      contentX, imageY,
-                      imageWidth, imageHeight, 0, 0,
-                      false, false, false, false);
-      }
-      else
-      {
-         imageWidth = 0;               
-      }
-
-      final int textX = getTreeViewNodeLabelHorizontalOffset(level, imageWidth);
-      
-      // node text highlight
-      if (nodeSelected)
-      {
-         gd.setColor(selectionBack);
-         gd.fillRect(textX, nodeHeight * rowIdx, rowWidth, nodeHeight);
-      }
-
-      // node text color
-      final ColorRgb textColor;
-      if (enabled && nodeSelected)
-      {
-         textColor = selectionFore;
-      }
-      else if (enabled)
-      {
-         textColor = gc.fgColor;
-      }
-      else
-      {
-         textColor = disabledFore;
-      }
-
-      // node text
-      gd.setColor(textColor);
-      gd.drawString(text, textX + 2, textY);
-      
-      final int signX = TREEVIEW_INNER_PADDING +
-                  TREEVIEW_EXPAND_ICON_OFFSET +
-                  getTreeViewNodeHorizontalOffset(level - 1);
-
-      final int iconY = nodeHeight * rowIdx + (nodeHeight - TREEVIEW_EXPAND_ICON_SIZE) / 2;
+      final FontDetails font = fc.font;
+      gd.setGuiFont(font);
+      gd.setFontStyle(font.style);
+
+      // fill the background
+      final ColorRgb bgColor = gc.bgColor;
+      gd.setColor(bgColor);
+      gd.fillRect(0, 0, width, height);
+
+      final List<ColumnCache> columns = tree.columns;
+      if (columns.isEmpty() || topNode == null)
+      {
+         return;
+      }
+
+      // Collect visible nodes that fit into the widget height, their row numbers and parents
+      final Map<TreeNode, Integer> node2row = new IdentityHashMap<>();
+      final Set<TreeNode> parents = Collections.newSetFromMap(new IdentityHashMap<>()); 
+      final int rowHeight = getTreeViewNodeHeight(fc);
+      final int nodesFit = (height + rowHeight - 1) / rowHeight;
+      int row = 0; 
+      for (TreeNode node = topNode; node != null && row < nodesFit; node = node.nextVisible())
+      {
+         node2row.put(node, Integer.valueOf(row));
+         parents.add(node.parent);
+         row++; 
+      }
+      
+      // Assure ancestors of the top node are added too.
+      for (TreeNode p = topNode.parent; p != null; p = p.parent)
+      {
+         parents.add(p);
+      }
+      
       final int signOff = TREEVIEW_EXPAND_ICON_SIZE / 2;
-      final int hSignOff = signOff / 2;
 
-      // horizontal dots
+      // draw vertical lines from parents down to the last children
       gd.setColor(TREEVIEW_DOTS_COLOR);
-      for (int i = 0; i < (contentX - signX - signOff - 1) / 2; i++)
-      {
-         final int x = signX + signOff + (i + 1) * 2;
-         final int y = iconY + signOff;
-         gd.drawLine(x, y, x, y);
-      }
-
-      // vertical dots
-      if (showExpandIcon || lastChild)
-      {
-         final int topY;
-         if (prevExpIcoRowIdx < 0)
+      for (final TreeNode p : parents)
+      {
+         final int level = p.getLevel();
+         final int expandIconLeftX = TREEVIEW_INNER_PADDING + TREEVIEW_EXPAND_ICON_OFFSET
+                  + getTreeViewNodeHorizontalOffset(level);
+         final int verticalDotsX = expandIconLeftX + signOff;
+         // Make sure the line from the very first real tree node to the virtual tree root
+         // is not drawn
+         final int lineTop;
+         final int halfRowHeight = rowHeight / 2;
+         if (p.parent == null && topNode.parent == p && p.nodes.get(0) == topNode)
          {
-            // connecting to the very first node or the parent node text
-            topY = level == 0 ? nodeHeight / 2 : nodeHeight * (parentRowIdx + 1);
+            lineTop = halfRowHeight;
          }
          else
          {
-            // connecting to the previous expand icon
-            topY = nodeHeight * (prevExpIcoRowIdx + 1) - ((nodeHeight - TREEVIEW_EXPAND_ICON_SIZE) / 2);
+            final Integer parentRow = node2row.get(p);
+            lineTop = parentRow != null ? rowHeight * parentRow.intValue() + halfRowHeight : 0;
          }
+         final List<TreeNode> children = p.nodes;
+         final TreeNode lastChild = children.get(children.size() - 1);
 
-         // when first child give room to the parent node text
-         for (int i = topY; i < (showExpandIcon ? iconY : iconY + signOff + 1); i += 2)
+         // Skip lines completely above the visible area
+         if (lastChild != topNode && lastChild.isDFTBeforeThan(topNode))
          {
-            final int x = signX + signOff;
-            gd.drawLine(x, i, x, i);
+            continue;
          }
+         
+         final Integer lastChildRow = node2row.get(lastChild);
+         final int bottom = lastChildRow != null
+                  ? rowHeight * lastChildRow.intValue() + halfRowHeight
+                  : height;
+         drawDottedVerticalLine(gd, verticalDotsX, lineTop, bottom);
       }
-
-      if (showExpandIcon)
+      
+      final TreeConfig c = tree.config();
+      final boolean configShowExpandIcon = c.showExpandIcon;
+      final int hSignOff = signOff / 2;
+      
+      //////////////////////////////////////////////////
+      // Do the drawing
+      //////////////////////////////////////////////////
+      
+      final ColorRgb fgColor = gc.fgColor;
+      for (final TreeNode node : node2row.keySet())
       {
-         // the expand icon border
+         final int level = node.getLevel();
+         final int contentX = getTreeViewNodeContentHorizontalOffset(level);
+         final int expandIconLeftX = TREEVIEW_INNER_PADDING + TREEVIEW_EXPAND_ICON_OFFSET
+                  + getTreeViewNodeHorizontalOffset(level - 1);
+
+         row = node2row.get(node).intValue();
+         final int rowTop = rowHeight * row;
+         final int iconY = rowTop + (rowHeight - TREEVIEW_EXPAND_ICON_SIZE) / 2;
+
+         // horizontal dots from the center of expand icon to the center of node icon.
          gd.setColor(TREEVIEW_DOTS_COLOR);
-         gd.drawRect(signX, iconY, TREEVIEW_EXPAND_ICON_SIZE - 1, TREEVIEW_EXPAND_ICON_SIZE - 1);
-
-         // the - collapse sign
-         gd.setColor(TREEVIEW_EXPAND_ICON_COLOR);
-         gd.drawLine(signX + hSignOff, iconY + signOff, signX + hSignOff + signOff, iconY + signOff);
-
-         // the + expand sign
-         if (!node.value.expanded)
-         {
-            gd.drawLine(signX + signOff, iconY + hSignOff, signX + signOff, iconY + hSignOff + signOff);
+         final int horizontalDotsY = iconY + signOff;
+         final int startX = expandIconLeftX + hSignOff + 2;
+         final int endX = signOff + contentX + 3;
+         for (int x = startX; x < endX; x += 2)
+         {
+            gd.drawLine(x, horizontalDotsY, x, horizontalDotsY);
+         }
+         
+         final TreeNodeEntry nodeValue = node.value;
+         final boolean nodeSelected = node == currentNode;
+         final ImageWrapper img = tree.getNodeImage(node, nodeSelected);
+         final int imageWidth = img != null ? img.getWidth() : 0;         
+
+         boolean firstColumn = true;
+         int xOff = 0;
+         for (final TreeListGuiImpl.ColumnCache cc : columns)
+         {
+            final Column column = cc.column;
+            if (!column.visible)
+            {
+               continue;
+            }
+
+            final int textXoffset;
+            if (firstColumn)
+            {
+               textXoffset = getTreeViewNodeLabelHorizontalOffset(level, imageWidth);
+               firstColumn = false;
+               // node icon
+               if (img != null)
+               {
+                  final int imageHeight = img.getHeight();
+                  final int imageY = rowTop + (rowHeight - imageHeight) / 2;
+                  gd.drawImage(img, contentX - 2, imageY, imageWidth, imageHeight, 0, 0, false,
+                           false, false, false);
+               }
+
+               // Expand/collapse button
+               if (configShowExpandIcon && nodeValue.hasChildren)
+               {
+                  final int expandIconDimension = TREEVIEW_EXPAND_ICON_SIZE - 1;
+
+                  // Button background
+                  gd.setColor(bgColor);
+                  gd.fillRect(expandIconLeftX, iconY, expandIconDimension, expandIconDimension);
+
+                  // Button border
+                  gd.setColor(TREEVIEW_DOTS_COLOR);
+                  gd.drawRect(expandIconLeftX, iconY, expandIconDimension, expandIconDimension);
+
+                  // the "-" sign line
+                  gd.setColor(TREEVIEW_EXPAND_ICON_COLOR);
+                  gd.drawLine(expandIconLeftX + hSignOff, iconY + signOff,
+                           expandIconLeftX + hSignOff + signOff, iconY + signOff);
+
+                  // the line tom make "+" sign
+                  if (!node.value.expanded)
+                  {
+                     gd.drawLine(expandIconLeftX + signOff, iconY + hSignOff,
+                              expandIconLeftX + signOff, iconY + hSignOff + signOff);
+                  }
+               }
+            }
+            else
+            {
+               textXoffset = 0;
+            }
+ 
+            final int index = cc.index;
+            final Object value = nodeValue.getValue(index);      
+            final int colWidth = column.width;
+
+            final NativePoint p = new NativePoint(xOff + textXoffset, rowTop);
+            final NativeDimension dim = new NativeDimension(colWidth - textXoffset, rowHeight);
+            final NativeRectangle clip = AbstractWidget.clipRectangle(p, dim);
+            
+            gd.draw(p, clip, () -> {
+               final List<ColorRgb> bgColors = ((TreeListNodeEntry) nodeValue).cellBGColors;
+               ColorRgb effBgColor = index < bgColors.size() ? bgColors.get(index) : null;
+
+               if (effBgColor == null)
+               {
+                  final ColorRgb columnBgColor = column.bgColor;
+                  effBgColor = columnBgColor != null ? columnBgColor : bgColor;
+               }
+
+               final List<ColorRgb> fgColors = ((TreeListNodeEntry) nodeValue).cellFGColors;
+               ColorRgb effFgColor = index < fgColors.size() ? fgColors.get(index) : null;
+               if (effFgColor == null)
+               {
+                  final ColorRgb columnFgColor = column.fgColor;
+                  effFgColor = columnFgColor != null ? columnFgColor : fgColor;
+               }
+
+               drawTreeListCell(gd, fc, tree, colWidth, rowHeight, effFgColor, selectionBack,
+                        effBgColor, value, column.align, nodeSelected, column.dataType);
+            });
+
+            xOff += colWidth;
          }
       }
    }
@@ -6959,6 +7203,7 @@
     * @param   flat
     *          Is tab button flat?
     */
+   @SuppressWarnings("static-method")
    private void drawTabSetButton(GuiDriver gd,
                                  int x,
                                  int y,
@@ -7145,88 +7390,6 @@
    }
    
    /**
-    * Draws TREELIST body.
-    *
-    * @param   gd
-    *          GUI driver.
-    * @param   gc
-    *          Color resolver.
-    * @param   font
-    *          Text font.
-    * @param   tree
-    *          Tree widget instance.
-    * @param   nodes
-    *          Root tree nodes to render.
-    * @param   currentNode
-    *          Selected tree node.
-    * @param   bodyDim
-    *          Tree widget body dimension.
-    * @param   selectionBg
-    *          Selection tree node background color.
-    * @param   selectionFg
-    *          Selection tree node foreground color.
-    * @param   disabledFore
-    *          Text color for the disabled tree node.
-    * @param   columns
-    *          Tree list columns.
-    */
-   public <N extends TreeNodeEntry> void drawTreeListBody(GuiDriver gd,
-                                                          final GuiColorResolver gc,
-                                                          final GuiFontResolver.FontCache font,
-                                                          final TreeListGuiImpl tree,
-                                                          final List<TreeGuiImpl.TreeNode<N>> nodes,
-                                                          final TreeGuiImpl.TreeNode<N> currentNode,
-                                                          final NativeDimension bodyDim,
-                                                          final ColorRgb selectionBg,
-                                                          final ColorRgb selectionFg,
-                                                          final ColorRgb disabledFore,
-                                                          final List<TreeListGuiImpl.ColumnCache> columns)
-   {
-      gd.setGuiFont(font.font);
-      gd.setFontStyle(font.font.style);
-
-      // fill the background
-      gd.setColor(gc.bgColor);
-      gd.fillRect(0, 0, bodyDim.width, bodyDim.height);
-
-      if (columns.isEmpty())
-      {
-         return;
-      }
-
-      // the first column will be rendered as tree view body
-      // Note: we need to limit the width by the first column only width.
-      final int firstVisibleColumnIndex = tree.getFirstVisibleColumnIndex();
-      final int width = columns.get(firstVisibleColumnIndex).column.width;
-      drawTreeViewBody(gd,
-                       gc,
-                       font,
-                       tree,
-                       nodes,
-                       currentNode,
-                       firstVisibleColumnIndex,
-                       width,
-                       selectionBg,
-                       selectionFg,
-                       disabledFore);
-
-
-      final int nodeHeight = getTreeViewNodeHeight(font);
-
-      drawTreeListNodes(gd,
-                        gc,
-                        font,
-                        tree,
-                        nodes,
-                        currentNode,
-                        0,
-                        selectionBg,
-                        selectionFg,
-                        columns,
-                        nodeHeight);
-   }
-
-   /**
     * Draws TREELIST caption.
     *
     * @param   gd
@@ -7238,10 +7401,11 @@
     * @param   height
     *          Caption height.
     */
-   public final void drawTreeListCaption(GuiDriver gd,
-                                   GuiColorResolver gc,
-                                   int width,
-                                   int height)
+   @SuppressWarnings("static-method")
+   public final void drawTreeListCaption(final GuiDriver gd,
+                                         final GuiColorResolver gc,
+                                         final int width,
+                                         final int height)
    {
       gd.setColor(TREELIST_CAPTION_COLOR);
       gd.fillRect(0, 0, width, height);
@@ -7265,6 +7429,7 @@
     * @param   isLast
     *          the caption it the last caption from the right
     */
+   @SuppressWarnings("static-method")
    public final void drawTreeListCaptionItem(final GuiDriver gd,
                                              final GuiColorResolver gc,
                                              final GuiFontResolver.FontCache font,
@@ -7400,7 +7565,7 @@
     *
     * @param   gd
     *          GUI driver.
-    * @param   font
+    * @param   fc
     *          Text font.
     * @param   tree
     *          Treelist instance.
@@ -7422,22 +7587,22 @@
     *            The column data type: 0 for string, 4 for icon
     */
    private void drawTreeListCell(final GuiDriver gd,
-                                 final GuiFontResolver.FontCache font,
+                                 final GuiFontResolver.FontCache fc,
                                  final TreeListGuiImpl tree,
                                  final int width,
                                  final int rowHeight,
                                  final ColorRgb fgColor,
-                                 final ColorRgb bgSelColor,
+                                 final ColorRgb selectionBack,
                                  final ColorRgb bgColor,
                                  Object value,
                                  final byte align,
                                  final boolean selected,
                                  final TreeListConfig.Column.DataType dataType)
    {
-      final int labelHeight = getTreeViewNodeLabelHeight(font);
+      final int labelHeight = getTreeViewNodeLabelHeight(fc);
       final int fillBgAreaHeight = Math.max(rowHeight, labelHeight);
 
-      gd.setColor(selected ? bgSelColor : bgColor);
+      gd.setColor(selected ? selectionBack : bgColor);
       gd.fillRect(0, 0, width, fillBgAreaHeight);
 
       if (value == null)
@@ -7452,7 +7617,7 @@
             gd.setColor(fgColor);
 
             final String textToDraw = value.toString().replaceFirst("\\s+$", "");
-            final int textWidth = tree.getTextWidth(textToDraw, font.font);
+            final int textWidth = tree.getTextWidth(textToDraw, fc.font);
 
             final int textXOffset;
             switch (align)
@@ -7472,7 +7637,7 @@
                   textXOffset = TREELIST_COLUMN_CAPTION_LEFT_PADDING;
             }
 
-            final int textY = getTreeViewNodeVerticalOffset(0, rowHeight, font);
+            final int textY = getTreeViewNodeVerticalOffset(0, rowHeight, fc);
             gd.drawString(textToDraw, Math.max(TREELIST_COLUMN_CAPTION_LEFT_PADDING, textXOffset),
                      textY);
             return;
@@ -7485,7 +7650,7 @@
                {
                   id = Integer.parseInt(((String) value).trim());
                }
-               catch (NumberFormatException e)
+               catch (@SuppressWarnings("unused") NumberFormatException e)
                {
                   return;
                }
@@ -7629,292 +7794,6 @@
    }
 
    /**
-    * Draws treeview nodes.
-    *
-    * @param   gd
-    *          GUI driver.
-    * @param   gc
-    *          Color resolver.
-    * @param   tree
-    *          Tree widget, the owner of the tree body.
-    * @param   nodes
-    *          Nodes to draw.
-    * @param   currentNode
-    *          Current node.
-    * @param   valueIndex
-    *          The index of the value to be displayed. For TREEVIEW widget this is always 0.
-    * @param   rowIdx
-    *          Row index.
-    * @param   font
-    *          Text font.
-    * @param   rowHeight
-    *          Row height.
-    * @param   rowWidth
-    *          Row width.
-    * @param   level
-    *          Tree level.
-    * @param   enabled
-    *          Widget enabled state.
-    * @param   selectionBack
-    *          Selection back color.
-    * @param   selectionFore
-    *          Selection foreground color.
-    * @param   disabledFore
-    *          Disabled widget foreground color.
-    */
-   private <N extends TreeNodeEntry> int drawTreeViewNodes(final GuiDriver gd,
-                                                           final GuiColorResolver gc,
-                                                           final TreeGuiImpl tree,
-                                                           final List<TreeGuiImpl.TreeNode<N>> nodes,
-                                                           final TreeGuiImpl.TreeNode<N> currentNode,
-                                                           final int valueIndex,
-                                                           final int rowIdx,
-                                                           final GuiFontResolver.FontCache font,
-                                                           final int rowHeight,
-                                                           final int rowWidth,
-                                                           final int level,
-                                                           final boolean enabled,
-                                                           final ColorRgb selectionBack,
-                                                           final ColorRgb selectionFore,
-                                                           final ColorRgb disabledFore)
-   {
-      int row = rowIdx;
-      final int nodeCount = nodes.size();
-      int parentRowIdx = row - 1;
-      int prevExpIcoRowIdx = -1;
-
-      final TreeConfig c = tree.config();
-      final int imageListId = c.imageListId;
-
-      for (int i = 0; i < nodeCount; i++)
-      {
-         final TreeGuiImpl.TreeNode node = nodes.get(i);
-         final boolean showExpandIcon = c.showExpandIcon && node.value.hasChildren;
-
-         drawTreeViewNode(gd,
-                          gc,
-                          tree,
-                          node,
-                          currentNode,
-                          valueIndex,
-                          row,
-                          parentRowIdx,
-                          prevExpIcoRowIdx,
-                          font,
-                          rowHeight,
-                          rowWidth,
-                          level,
-                          enabled,
-                          i == 0,
-                          i == nodeCount - 1,
-                          showExpandIcon,
-                          selectionBack,
-                          selectionFore,
-                          disabledFore,
-                          imageListId);
-
-         if (showExpandIcon)
-         {
-            prevExpIcoRowIdx = row;
-         }
-         row++;
-
-         if (node.value.expanded && node.nodes != null)
-         {
-            row = drawTreeViewNodes(gd,
-                                       gc,
-                                       tree,
-                                       node.nodes,
-                                       currentNode,
-                                       valueIndex,
-                                       row,
-                                       font,
-                                       rowHeight,
-                                       rowWidth,
-                                       level + 1,
-                                       enabled,
-                                       selectionBack,
-                                       selectionFore,
-                                       disabledFore);
-         }
-      }
-
-      return row;
-   }
-
-   /**
-    * Draws TREELIST nodes.
-    *
-    * @param   gd
-    *          GUI driver.
-    * @param   gc
-    *          Color resolver.
-    * @param   font
-    *          Text font.
-    * @param   tree
-    *          Tree widget instance.
-    * @param   nodes
-    *          Root tree nodes to render.
-    * @param   currentNode
-    *          Selected tree node.
-    * @param   nodeIndex
-    *          Index of the first node in the supplied nodes list.
-    * @param   selectionBg
-    *          Selection tree node background color.
-    * @param   selectionFg
-    *          Selection tree node foreground color.
-    * @param   columns
-    *          Tree list columns.
-    * @param   nodeHeight
-    *          Node height in pixels.
-    */
-   private <N extends TreeNodeEntry> int drawTreeListNodes(final GuiDriver gd,
-                                                           final GuiColorResolver gc,
-                                                           final GuiFontResolver.FontCache font,
-                                                           final TreeListGuiImpl tree,
-                                                           final List<TreeGuiImpl.TreeNode<N>> nodes,
-                                                           final TreeGuiImpl.TreeNode<N> currentNode,
-                                                           final int nodeIndex,
-                                                           final ColorRgb selectionBg,
-                                                           final ColorRgb selectionFg,
-                                                           final List<TreeListGuiImpl.ColumnCache> columns,
-                                                           final int nodeHeight)
-   {
-      int idx = nodeIndex;
-      if (nodes != null)
-      {
-         for (final TreeGuiImpl.TreeNode node : nodes)
-         {
-            final boolean isSelected = node == currentNode;
-
-            // FIXME: the background color passed here must be affected by the node color. 
-            final ColorRgb bgColor = gc.bgColor;
-            final ColorRgb fgColor = gc.fgColor;
-            drawTreeListNode(gd, font, tree, columns, nodeHeight, idx, node,
-                     isSelected ? selectionFg : fgColor, isSelected ? selectionBg : bgColor,
-                     bgColor, isSelected);
-            idx++;
-
-            if (node.value.expanded && node.nodes != null)
-            {
-               idx = drawTreeListNodes(gd,
-                                             gc,
-                                             font,
-                                             tree,
-                                             node.nodes,
-                                             currentNode,
-                                             idx,
-                                             selectionBg,
-                                             selectionFg,
-                                             columns,
-                                             nodeHeight);
-            }
-         }
-      }
-
-      return idx;
-   }
-
-   /**
-    * Draws TREELIST node.
-    *
-    * @param   gd
-    *          GUI driver.
-    * @param   font
-    *          Text font.
-    * @param   tree
-    *          Tree widget instance.
-    * @param   columns
-    *          Tree list columns.
-    * @param   nodeHeight
-    *          Node height.
-    * @param   nodeIndex
-    *          Node index.
-    * @param   node
-    *          Tree node.
-    * @param   fgColor
-    *          Foreground color.
-    * @param   bgSelColor
-    *          Background color for selected node.
-    * @param   bgColor
-    *          Background color for regular node.
-    * @param   selected
-    *          If <code>TRUE</code> the node is selected, otherwise - not..
-    */
-   private void drawTreeListNode(final GuiDriver gd,
-                                 final GuiFontResolver.FontCache font,
-                                 final TreeListGuiImpl tree,
-                                 final List<TreeListGuiImpl.ColumnCache> columns,
-                                 final int nodeHeight,
-                                 final int nodeIndex,
-                                 final TreeGuiImpl.TreeNode node,
-                                 final ColorRgb fgColor,
-                                 final ColorRgb bgSelColor,
-                                 final ColorRgb bgColor,
-                                 final boolean selected)
-   {
-      final int nColumns = columns.size();
-      if (nColumns < 2)
-      {
-         return;
-      }
-
-      final TreeListGuiImpl.ColumnCache firstColumn = columns.get(0);
-      int xOff = firstColumn.column.width;
-      for (final TreeListGuiImpl.ColumnCache cc : columns.subList(1, nColumns))
-      {
-         final Column column = cc.column;
-         if (!column.visible)
-         {
-            continue;
-         }
-
-         final TreeListNodeEntry entry = (TreeListNodeEntry) node.value;
-         final List<Object> cellValues = entry.cellValues;
-         final int index = cc.index;
-         final Object value = index < cellValues.size() ? cellValues.get(index) : null;
-
-         final int colWidth = column.width;
-
-         final NativePoint p = new NativePoint(xOff, nodeHeight * nodeIndex);
-         final NativeDimension dim = new NativeDimension(colWidth, nodeHeight);
-         final NativeRectangle clip = AbstractWidget.clipRectangle(p, dim);
-         
-         gd.draw(p, clip, () -> {
-            final List<ColorRgb> bgColors = entry.cellBGColors;
-            ColorRgb effBgColor = null;
-            if (index < bgColors.size())
-            {
-               effBgColor = bgColors.get(index);
-            }
-
-            if (effBgColor == null)
-            {
-               final ColorRgb columnBgColor = column.bgColor;
-               effBgColor = columnBgColor != null ? columnBgColor : bgColor;
-            }
-
-            final List<ColorRgb> fgColors = entry.cellBGColors;
-            ColorRgb effFgColor = null;
-            if (index < fgColors.size())
-            {
-               effFgColor = fgColors.get(index);
-            }
-            if (effFgColor == null)
-            {
-               final ColorRgb columnFgColor = column.fgColor;
-               effFgColor = columnFgColor != null ? columnFgColor : fgColor;
-            }
-
-            drawTreeListCell(gd, font, tree, colWidth, nodeHeight, effFgColor, bgSelColor,
-                     effBgColor, value, column.align, selected, column.dataType);
-         });
-
-         xOff += colWidth;
-      }
-   }
-   
-   /**
     * Draws progress bar {@code ProgressBarGui} widget.
     *
     * @param   pb
@@ -7997,6 +7876,7 @@
     * @param   height
     *          The height of the painted widget/component.
     */
+   @SuppressWarnings("static-method")
    private void drawProgressBarBar(ProgressBarGui pb, 
                                    GuiDriver gd, 
                                    GuiColorResolver gc,

=== modified file 'src/com/goldencode/p2j/ui/client/gui/theme/Theme.java'
--- src/com/goldencode/p2j/ui/client/gui/theme/Theme.java	2020-10-24 22:32:57 +0000
+++ src/com/goldencode/p2j/ui/client/gui/theme/Theme.java	2021-06-17 17:52:42 +0000
@@ -163,7 +163,10 @@
    /**
     * Release any held resources when this {@code Theme} is removed. 
     */
-   public default void releaseResources() {};
+   public default void releaseResources()
+   {
+      // empty
+   };
    
    /**
     * Obtain an image resource specific to this {@code Theme}. The image can be cached for

=== added file 'src/com/goldencode/p2j/ui/client/model/I2DScrollable.java'
--- src/com/goldencode/p2j/ui/client/model/I2DScrollable.java	1970-01-01 00:00:00 +0000
+++ src/com/goldencode/p2j/ui/client/model/I2DScrollable.java	2021-06-10 20:49:41 +0000
@@ -0,0 +1,25 @@
+package com.goldencode.p2j.ui.client.model;
+
+/**
+ * Two-dimensional scrollable object. This interface defines two methods {@link #updateHScrollModel()} and
+ * {@link #updateVScrollModel()}, which should be called by application any time the horizontal/vertical
+ * scrolling models should be updated.
+ */
+public interface I2DScrollable
+extends IModelChangeListener
+{
+   /**
+    * Update the vertical scrolling model based on this object geometry and contents.
+    * 
+    * @return {@code true} if model was changed as the result of the call.
+    */
+   boolean updateVScrollModel();
+
+   /**
+    * Update the horizontal scrolling model based on this object geometry and contents.
+    * 
+    * @return {@code true} if model was changed as the result of the call.
+    */
+   boolean updateHScrollModel();
+
+}
\ No newline at end of file

=== added file 'src/com/goldencode/p2j/ui/client/model/IModelChangeListener.java'
--- src/com/goldencode/p2j/ui/client/model/IModelChangeListener.java	1970-01-01 00:00:00 +0000
+++ src/com/goldencode/p2j/ui/client/model/IModelChangeListener.java	2021-06-09 16:43:25 +0000
@@ -0,0 +1,16 @@
+package com.goldencode.p2j.ui.client.model;
+
+/**
+ * See {@link Model}.
+ *
+ */
+public interface IModelChangeListener
+{
+   /**
+    * This procedure is called by the model after it was changed.
+    * 
+    * @param model the model that was changed
+    */
+   void modelChanged(Model model);
+
+}

=== added file 'src/com/goldencode/p2j/ui/client/model/IntegerModel.java'
--- src/com/goldencode/p2j/ui/client/model/IntegerModel.java	1970-01-01 00:00:00 +0000
+++ src/com/goldencode/p2j/ui/client/model/IntegerModel.java	2021-06-09 17:08:50 +0000
@@ -0,0 +1,50 @@
+package com.goldencode.p2j.ui.client.model;
+
+/**
+ * Integer number model.
+ * 
+ */
+public final class IntegerModel extends Model
+{
+   /**
+    * The value.
+    */
+   private int value;
+
+   /**
+    * The constructor.
+    */
+   public IntegerModel(int value)
+   {
+      this.value = value;
+   }
+
+   /**
+    * Set the value.
+    * 
+    * @param index the new value
+    * 
+    * @return {@code true} if model was changed as the result of this call.
+    */
+   public boolean setValue(int index)
+   {
+      if (this.value == index)
+      {
+         return false;
+      }
+
+      this.value = index;
+      changed();
+      return true;
+   }
+
+   /**
+    * Get the value index value.
+    * 
+    * @return see above
+    */
+   public int getValue()
+   {
+      return value;
+   }
+}

=== added file 'src/com/goldencode/p2j/ui/client/model/Model.java'
--- src/com/goldencode/p2j/ui/client/model/Model.java	1970-01-01 00:00:00 +0000
+++ src/com/goldencode/p2j/ui/client/model/Model.java	2021-06-09 16:44:37 +0000
@@ -0,0 +1,75 @@
+/**
+ * 
+ */
+package com.goldencode.p2j.ui.client.model;
+
+import java.util.*;
+
+/**
+ *
+ */
+public abstract class Model
+{
+
+   private List<IModelChangeListener> listeners;
+
+   /**
+    * Notify model listeners.
+    */
+   public void changed()
+   {
+      // info("{0}", this);
+
+      if (listeners != null)
+      {
+         for (IModelChangeListener listener : listeners)
+         {
+            listener.modelChanged(this);
+         }
+      }
+
+   }
+
+   @SuppressWarnings("null")
+   private final List<IModelChangeListener> getListenersForUpdate()
+   {
+      if (listeners == null)
+      {
+         listeners = new ArrayList<>();
+      }
+
+      return listeners;
+   }
+
+   /**
+    * Add listener for scrolling changes.
+    * It is an error to add the same object twice.
+    * 
+    * @param listener the object that will be notified
+    */
+   public void addListener(IModelChangeListener listener)
+   {
+      final List<IModelChangeListener> l = getListenersForUpdate();
+      if (!l.contains(listener))
+      {
+         l.add(listener);
+      }
+   }
+
+   /**
+    * Remove the object from the listener list.
+    * 
+    * @param listener the object to be removed.
+    */
+   public void removeListener(IModelChangeListener listener)
+   {
+      getListenersForUpdate().remove(listener);
+   }
+
+   /**
+    * Default constructor.
+    */
+   public Model()
+   {
+   }
+}
\ No newline at end of file

=== added file 'src/com/goldencode/p2j/ui/client/model/ScrollModel.java'
--- src/com/goldencode/p2j/ui/client/model/ScrollModel.java	1970-01-01 00:00:00 +0000
+++ src/com/goldencode/p2j/ui/client/model/ScrollModel.java	2021-06-09 17:21:15 +0000
@@ -0,0 +1,188 @@
+package com.goldencode.p2j.ui.client.model;
+
+/**
+ * Scrollable model.
+ * 
+ * The model includes the following data:
+ * 
+ * <ul>
+ * <li>{@link #width} - the total "view width"
+ * <li>{@link #page} - the part of the width which is "visible"
+ * <li>{@link #position} - the offset of the visible part
+ * </ul>
+ * 
+ * <p>
+ * The following relations must hold between the fields:
+ * <ul>
+ * <li>{@link width} &gt;= 0 width is non-negative
+ * <li>{@link page} &gt;= 0 page is non-negative
+ * <li>{@link position} &gt;= 0
+ * <li>{@link position} + {@link page} &lt;= {@link width}
+ * </ul>
+ *
+ */
+public final class ScrollModel extends Model
+{
+   /**
+    * the size of the scrolled object in some object-specific units, &gt;= 0
+    */
+   private int width;
+
+   /**
+    * the size of visible region in object-specific units, page &lt;= width
+    */
+   private int page;
+
+   /**
+    * Current scroll position 0 &lt;= position &lt;= (width - page)
+    */
+   private int position;
+
+   /**
+    * Create a scrolling model in some "no scrolling required" state.
+    * It is assumed the model will be updated to the actual state before
+    * being used.
+    */
+   public ScrollModel()
+   {
+      this(0, 0, 0);
+   }
+
+   /**
+    * Create a scrolling model.
+    * 
+    * @param width the width value
+    * @param page the page value
+    * @param position the position.
+    */
+   public ScrollModel(final int width, final int page, final int position)
+   {
+      assert width >= 0;
+      assert page >= 0;
+      assert 0 <= position && position <= (width - page);
+
+      this.width = width;
+      this.page = page;
+      this.position = position;
+   }
+
+   /**
+    * Get the scroll model width.
+    * 
+    * @return see above.
+    */
+   public int getWidth()
+   {
+      return width;
+   }
+
+   /**
+    * Get the scroll model page value.
+    * 
+    * @return see above.
+    */
+   public int getPage()
+   {
+      return page;
+   }
+
+   /**
+    * Set all model data at once.
+    * 
+    * @param width the new width value
+    * @param page the new page value
+    * @param position the new position value
+    * 
+    * @return {@code true} if model was changed as the result of this call.
+    */
+   public boolean set(int width, int page, int position)
+   {
+      if (this.width == width && this.page == page && this.position == position)
+      {
+         return false;
+      }
+
+      assert width >= 0;
+      assert page >= 0;
+      assert 0 <= position;
+      assert position <= (width - page);
+
+      this.width = width;
+      this.page = page;
+      this.position = position;
+
+      changed();
+      return true;
+   }
+
+   /**
+    * Get the scroll model position value.
+    * 
+    * @return see above
+    */
+   public int getPosition()
+   {
+      return position;
+   }
+
+   /**
+    * Get the sum of the position and the page.
+    * 
+    * @return see above
+    */
+   public int getLastVisible()
+   {
+      return position + page;
+   }
+
+   /**
+    * Change the model position.
+    * 
+    * @param position the new position value.
+    * 
+    * @return {@code true} if position was changed as the result of this call.
+    */
+   public boolean setPosition(int position)
+   {
+      assert 0 <= position && position <= (width - page);
+      if (this.position == position)
+      {
+         return false;
+      }
+      this.position = position;
+      changed();
+      return true;
+   }
+
+   /**
+    * Test is any scrolling is possible. With no scrolling possible,
+    * attached scrollbar(s) may be hidden.
+    *  
+    * @return see above
+    */
+   public boolean isScrollAvailable()
+   {
+      return width > 1 && page < width;
+   }
+
+   /**
+    * Convenience function to shift the position by the given number.
+    * Assures the resulting position is valid, i.e. in the [0, width - page] range.
+    *  
+    * @param value any increment value, may be negative.
+    */
+   public boolean increment(int value)
+   {
+      return setPosition(Math.min(width - page, Math.max(0, position + value)));
+   }
+
+   /**
+    * Reset all fields to initial state without sending notifications.
+    */
+   public void reset()
+   {
+      position = 0;
+      page = 0;
+      width = 0;
+   }
+}

=== added file 'src/com/goldencode/p2j/ui/client/widget/AbstractScrollBar.java'
--- src/com/goldencode/p2j/ui/client/widget/AbstractScrollBar.java	1970-01-01 00:00:00 +0000
+++ src/com/goldencode/p2j/ui/client/widget/AbstractScrollBar.java	2021-06-09 17:16:17 +0000
@@ -0,0 +1,255 @@
+package com.goldencode.p2j.ui.client.widget;
+
+import java.awt.event.*;
+
+import com.goldencode.p2j.ui.client.*;
+import com.goldencode.p2j.ui.client.gui.*;
+import com.goldencode.p2j.ui.client.gui.driver.*;
+import com.goldencode.p2j.ui.client.gui.theme.*;
+import com.goldencode.p2j.ui.client.model.*;
+
+abstract class AbstractScrollBar
+extends GenericWidget
+implements IModelChangeListener
+{
+
+   /**
+    * Enumerate all possible mouse positions in the widget area.
+    */
+   protected enum Part
+   {
+      /** The area between the left (upper) button and the thumb */
+      LEFT_AREA,
+
+      /** The left (upper) button */
+      LEFT_BUTTON,
+
+      /** None of the above */
+      NONE,
+
+      /** The area between the right (lower) button and the thumb */
+      RIGHT_AREA,
+
+      /** The right (lower) button */
+      RIGHT_BUTTON,
+
+      /** The thumb */
+      THUMB
+
+   }
+
+   private static final int[] MOUSE_ACTIONS = new int[] { MouseEvent.MOUSE_ENTERED,
+            MouseEvent.MOUSE_EXITED, MouseEvent.MOUSE_MOVED, MouseEvent.MOUSE_DRAGGED,
+            MouseEvent.MOUSE_WHEEL };
+
+   @Override
+   public void destroy()
+   {
+      model.removeListener(this);
+      super.destroy();
+   }
+
+   @Override
+   public void mouseWheelMoved(/* @Nullable */ MouseWheelEvent e)
+   {
+      assert e != null;
+      model.increment(e.getWheelRotation());
+   }
+
+   /**
+    * Find the widget positioned just below the specified mouse position (in physical units).
+    * Since this is the only widget, always return {@code this}.
+    * 
+    * @param    p
+    *           The mouse physical location.
+    * 
+    * @return   A widget over which the mouse is positioned or <code>null</code> if none found.
+    */
+   @Override
+   public Widget<GuiOutputManager> findMouseSource(/* @Nullable */ NativePoint p)
+   {
+      return this;
+   }
+
+   public AbstractScrollBar(ScrollModel model)
+   {
+      this.model = model;
+      model.addListener(this);
+   }
+
+   protected ScrollModel model;
+
+   /** The part the mouse was last time hovered over. */
+   protected Part lastHoverPart = Part.NONE;
+   
+   /** The part the mouse was last time pressed. */
+   protected Part lastPressedPart = Part.NONE;
+   
+   /**
+    * If not {@code null}, the bounding rectangle of currently highlighted part,
+    * this rectangle must be invalidated when highlighted part or thumb position changes.
+    */
+   /* @Nullable */
+   protected NativeRectangle highLightedRect;
+
+   /**
+    * Last pressed mouse position along the scrolling direction.
+    * Used to calculate the drag amount.
+    */
+   protected int lastMousePos;
+
+   /**
+    * Last pressed mouse model position. Used to calculate drag amount.
+    */
+   protected int lastModelPos;
+
+   @Override
+   public void draw()
+   {
+      if (!isVisible())
+      {
+         return;
+      }
+
+      // get location and size
+      NativePoint p = physicalLocation();
+      NativeDimension d = physicalDimension();
+      NativeRectangle clip = clipRectangle(p, d);
+
+      GuiDriver<?, ?> gd = (GuiDriver<?, ?>) OutputManager.getDriver();
+      GuiColorResolver gc = new GuiColorResolver(gd, config(), SysColor.COLOR_SCROLLBAR,
+               SysColor.COLOR_WINDOWTEXT);
+      gc.thumbColor = gd.getSysColor(SysColor.COLOR_3DFACE);
+      gc.colors();
+
+      gd.draw(getId(), p, clip, () -> doDraw(gd, gc));
+   }
+
+   /**
+    * Draw widget using the prepared driver and color resolver.
+    *
+    * @param gd the driver
+    * @param gc the color resolver.
+    */
+   protected abstract void doDraw(GuiDriver<?, ?> gd, GuiColorResolver gc);
+
+   /**
+    * Get the scrollbar minimum length along the scrolling direction.
+    * 
+    * @return see above
+    */
+   protected static final int minLength()
+   {
+      Theme theme = ThemeManager.getCurrentTheme();
+      return 2 * theme.getScrollBarButtonHeight() + theme.getScrollBarThumbMinSize();
+   }
+
+   /**
+    * Get the scrollbar minimum height.
+    * 
+    * @return see above
+    */
+   protected static final int minWidth()
+   {
+      return ThemeManager.getCurrentTheme().getScrollBarThumbWidth();
+   }
+
+   @Override
+   public void modelChanged(Model m)
+   {
+      repaint();
+   }
+
+   @Override
+   protected int[] mouseActions()
+   {
+      return MOUSE_ACTIONS;
+   }
+
+   @Override
+   public void mouseEntered(/* @Nullable */ MouseEvent e)
+   {
+      assert e != null;
+      updatePart(e);
+   }
+
+   @Override
+   public void mouseExited(/* @Nullable */ MouseEvent e)
+   {
+      assert e != null;
+      updatePart(e);
+   }
+
+   @Override
+   public void mouseMoved(/* @Nullable */ MouseEvent e)
+   {
+      assert e != null;
+      updatePart(e);
+   }
+
+   @Override
+   public void mouseReleased(/* @Nullable */ MouseEvent e)
+   {
+      assert e != null;
+      updateMouseHitInfo(e.getX(), e.getY());
+      Part lpp = lastPressedPart;
+      if (lpp == Part.NONE)
+      {
+         return;
+      }
+
+      lastPressedPart = Part.NONE;
+
+      if (lastHoverPart == lpp)
+      {
+         switch (lpp)
+         {
+            case LEFT_BUTTON:
+               model.increment(-1);
+               break;
+            case RIGHT_BUTTON:
+               model.increment(1);
+               break;
+            case LEFT_AREA:
+               model.increment(-model.getPage());
+               break;
+            case RIGHT_AREA:
+               model.increment(model.getPage());
+               break;
+            default:
+               break;
+         }
+      }
+      
+      repaint();
+   }
+
+   protected abstract void updateMouseHitInfo(int x, int y);
+
+   /**
+    * Repaint parts according to their highlight and pressed status when mouse position changes.
+    * 
+    * @param e the mouse event with the mouse position.
+    */
+   private void updatePart(MouseEvent e)
+   {
+      NativeRectangle prevRect = highLightedRect;
+      Part prevPart = lastHoverPart;
+      updateMouseHitInfo(e.getX(), e.getY());
+      if (prevPart != lastHoverPart)
+      {
+         if (prevRect != null)
+         {
+            repaint(prevRect);
+         }
+
+         if (highLightedRect != null)
+         {
+            repaint(highLightedRect);
+         }
+         
+         repaint();
+      }
+   }
+
+}

=== added file 'src/com/goldencode/p2j/ui/client/widget/ArrowsWidget.java'
--- src/com/goldencode/p2j/ui/client/widget/ArrowsWidget.java	1970-01-01 00:00:00 +0000
+++ src/com/goldencode/p2j/ui/client/widget/ArrowsWidget.java	2021-06-09 19:04:18 +0000
@@ -0,0 +1,107 @@
+package com.goldencode.p2j.ui.client.widget;
+
+import com.goldencode.p2j.ui.*;
+import com.goldencode.p2j.ui.client.*;
+import com.goldencode.p2j.ui.client.gui.driver.*;
+
+/**
+ * The {@link ArrowsWidget} widget is used to visually track the arrows when
+ * dragging a column header.
+ * 
+ * An instance of this widget is assumed to be created when dragging starts and destroyed
+ * when dragging ends.
+ * 
+ */
+public final class ArrowsWidget
+extends LightweightWidget
+{
+   /**
+    * Arrow width
+    */
+   final static int ARROWS_WIDTH = 11;
+
+   /**
+    * Arrow height
+    */
+   final static int ARROWS_HEIGHT = 9;
+
+   /**
+    * The vertical distance between the arrows.
+    */
+   private int verticalGap;
+
+   /**
+    * The color to fill the arrows
+    */
+   private ColorRgb fillColor;
+
+   /**
+    * Default constructor.
+    * 
+    */
+   public ArrowsWidget(final int verticalGap, final ColorRgb fillColor)
+   {
+      this.verticalGap = verticalGap;
+      this.fillColor = fillColor;
+   }
+
+   @Override
+   public void draw()
+   {
+      final NativePoint p = physicalLocation();
+      final int left = p.x;
+      final int top = p.y;
+      final int bottom = top + nativeHeight();
+
+      @SuppressWarnings("null")
+      final GuiDriver<?, ?> gd = (GuiDriver<?, ?>) OutputManager.getDriver();
+
+      polygon(gd, new int[] { left + 5, left, left + 3, left + 3, left + 7, left + 7, left + 10 },
+               new int[] { top + 8, top + 3, top + 3, top, top, top + 3, top + 3 });
+
+      polygon(gd, new int[] { left + 5, left + 10, left + 7, left + 7, left + 3, left + 3, left },
+               new int[] { bottom - 9, bottom - 4, bottom - 4, bottom - 1, bottom - 1, bottom - 4,
+                        bottom - 4 });
+   }
+
+   private final void polygon(GuiDriver<?, ?> gd, final int[] xPoints, final int[] yPoints)
+   {
+      final int nPoints = xPoints.length;
+      assert nPoints > 2;
+      assert nPoints == yPoints.length;
+      gd.setColor(fillColor);
+      gd.fillPolygon(xPoints, yPoints);
+
+      gd.setColor(ColorRgb.BLACK);
+
+      int x = xPoints[0];
+      int y = yPoints[0];
+      for (int i = 1; i < nPoints; i++)
+      {
+         gd.drawLine(x, y, xPoints[i], yPoints[i]);
+         x = xPoints[i];
+         y = yPoints[i];
+      }
+
+      gd.drawLine(x, y, xPoints[0], yPoints[0]);
+   }
+
+   @SuppressWarnings("null")
+   @Override
+   public double height()
+   {
+      return screen().coordinates().heightFromNative(nativeHeight());
+   }
+
+   private final int nativeHeight()
+   {
+      return ARROWS_HEIGHT * 2 + verticalGap;
+   }
+
+   @Override
+   public double width()
+   {
+      return screen().coordinates().widthFromNative(ARROWS_WIDTH);
+   }
+
+}

=== added file 'src/com/goldencode/p2j/ui/client/widget/GenericContainer.java'
--- src/com/goldencode/p2j/ui/client/widget/GenericContainer.java	1970-01-01 00:00:00 +0000
+++ src/com/goldencode/p2j/ui/client/widget/GenericContainer.java	2021-06-09 16:59:33 +0000
@@ -0,0 +1,79 @@
+package com.goldencode.p2j.ui.client.widget;
+
+import com.goldencode.p2j.ui.*;
+import com.goldencode.p2j.ui.client.*;
+import com.goldencode.p2j.ui.client.gui.driver.*;
+
+/**
+ * This class is slightly more convenient replacement for
+ * {@link GenericWidget}.
+ *
+ */
+public class GenericContainer extends AbstractContainer<GuiOutputManager>
+         implements WidgetWithConfig<BaseConfig>
+{
+   public GenericContainer()
+   {
+      config.id = WidgetId.nextID();
+   }
+
+   /**
+    * The widget config. The application sets the {@link BaseConfig#widthChars}
+    * and {@link BaseConfig#heightChars} while laying out.
+    */
+   protected BaseConfig config = new BaseConfig();
+
+   @SuppressWarnings("unchecked")
+   @Override
+   public BaseConfig config()
+   {
+      return config;
+   }
+
+   /**
+    * Default no-op implementation.
+    */
+   @Override
+   public void initialize(/* @Nullable */ WidgetId id, BaseConfig cfg)
+   {
+      // no-op
+   }
+
+   /**
+    * Default no-op implementation.
+    */
+   @Override
+   public void afterConfigUpdate(BaseConfig beforeUpdate)
+   {
+      // no-op
+   }
+
+   @SuppressWarnings("null")
+   @Override
+   public GenericContainer add(Widget<GuiOutputManager> w)
+   {
+      super.add(w);
+      GuiOutputManager.instance().getRegistry().addWidget(w);
+      return this;
+   }
+
+   /**
+    * Draw children relative to this container location.
+    */
+   @Override
+   public void draw()
+   {
+      final NativeDimension d = physicalDimension();
+      final NativePoint p = physicalLocation();
+      final NativeRectangle clip = clipRectangle(p, d);
+   
+      GuiDriver<?, ?> gd = (GuiDriver<?, ?>) OutputManager.getDriver();
+      
+      /**
+       * Note: the lambda below cannot be replaced by the method reference.
+       * This causes synthetic access method creation.
+       */
+      gd.draw(p, clip, () -> super.draw());
+   }
+
+}
\ No newline at end of file

=== added file 'src/com/goldencode/p2j/ui/client/widget/GenericWidget.java'
--- src/com/goldencode/p2j/ui/client/widget/GenericWidget.java	1970-01-01 00:00:00 +0000
+++ src/com/goldencode/p2j/ui/client/widget/GenericWidget.java	2021-06-09 17:09:06 +0000
@@ -0,0 +1,75 @@
+package com.goldencode.p2j.ui.client.widget;
+
+import com.goldencode.p2j.ui.*;
+import com.goldencode.p2j.ui.client.*;
+
+/**
+ * This class is slightly more convenient replacement for
+ * {@link AbstractWidget}.
+ *
+ */
+public abstract class GenericWidget extends LightweightWidget
+         implements WidgetWithConfig<BaseConfig>
+{
+   public GenericWidget()
+   {
+      config.id = WidgetId.nextID();
+   }
+
+   /**
+    * The widget config. The application sets the {@link BaseConfig#widthChars}
+    * and {@link BaseConfig#heightChars} while laying out.
+    */
+   protected BaseConfig config = new BaseConfig();
+
+   @SuppressWarnings("unchecked")
+   @Override
+   public BaseConfig config()
+   {
+      return config;
+   }
+
+   @Override
+   public double height()
+   {
+      return config.heightChars;
+   }
+
+   @Override
+   public double width()
+   {
+      return config.widthChars;
+   }
+
+   /**
+    * This implementation is required by the {@link WidgetWithConfig} protocol,
+    * but is neither called or used for non-containers.
+    */
+   @Override
+   public void initialize(/* @Nullable */ WidgetId id, BaseConfig cfg)
+   {
+      // no-op
+   }
+
+   /**
+    * Default no-op implementation.
+    */
+   @Override
+   public void afterConfigUpdate(BaseConfig beforeUpdate)
+   {
+      // no-op
+   }
+
+   /**
+    * Set the widget dimensions. Default implementation changes the config
+    * fields.
+    * 
+    * @param width the new widget width
+    * @param height the new widget height
+    */
+   public void setSize(double width, double height)
+   {
+      config.widthChars = width;
+      config.heightChars = height;
+   }
+}
\ No newline at end of file

=== added file 'src/com/goldencode/p2j/ui/client/widget/HScrollBar.java'
--- src/com/goldencode/p2j/ui/client/widget/HScrollBar.java	1970-01-01 00:00:00 +0000
+++ src/com/goldencode/p2j/ui/client/widget/HScrollBar.java	2021-06-09 17:34:32 +0000
@@ -0,0 +1,443 @@
+package com.goldencode.p2j.ui.client.widget;
+
+import java.awt.event.*;
+
+import com.goldencode.p2j.ui.*;
+import com.goldencode.p2j.ui.client.*;
+import com.goldencode.p2j.ui.client.gui.*;
+import com.goldencode.p2j.ui.client.gui.driver.*;
+import com.goldencode.p2j.ui.client.gui.theme.*;
+import com.goldencode.p2j.ui.client.model.*;
+
+/**
+ * This class is a complete horizontal scrollbar implementation.
+ *
+ */
+public final class HScrollBar extends AbstractScrollBar
+{
+   /**
+    * The (only) constructor for this widget.
+    * 
+    * @param model the scrolling model.
+    */
+   public HScrollBar(ScrollModel model)
+   {
+      super(model);
+   }
+
+   @Override
+   protected void doDraw(GuiDriver<?, ?> gd, GuiColorResolver gc)
+   {
+      final NativeDimension dim = physicalDimension();
+      final int scrollBarWidth = dim.width;
+      final Theme theme = ThemeManager.getCurrentTheme();
+
+      // fill in scroll bar area
+      theme.drawScrollBarGauge(gd, gc, 0, 0, scrollBarWidth, dim.height, true /* isHorizontal */);
+
+      final int buttonLongDim = Math.min(scrollBarWidth / 2, theme.getScrollBarButtonHeight());
+
+      /**
+       * The length between the left and right buttons
+       */
+      final int interButtonLength = scrollBarWidth - 2 * buttonLongDim;
+
+      /**
+       * Check for the case the widget width is not enough for just two buttons.
+       * In this case neither buttons nor thumb should not be visible.
+       */
+      if (interButtonLength < 0)
+      {
+         return;
+      }
+
+      final int buttonShortDim = theme.getScrollBarButtonWidth();
+
+      /**
+       * We center the horizontal scrollbar vertically
+       */
+      final int buttonY = (dim.height - buttonShortDim) / 2;
+
+      // draw left/top button
+      ThemeManager.getCurrentTheme().drawScrollBarButton(gd, gc, 0, buttonY, buttonLongDim,
+               buttonShortDim, ScrollBar.Position.LEFT, true, true,
+               lastPressedPart == Part.LEFT_BUTTON, lastHoverPart == Part.LEFT_BUTTON);
+
+      // draw right/bottom button
+      final int thumbEndLimit = scrollBarWidth - buttonLongDim;
+      ThemeManager.getCurrentTheme().drawScrollBarButton(gd, gc, thumbEndLimit, buttonY,
+               buttonLongDim, buttonShortDim, ScrollBar.Position.RIGHT, true, true,
+               lastPressedPart == Part.RIGHT_BUTTON, lastHoverPart == Part.RIGHT_BUTTON);
+
+      final int thumbMinWidth = theme.getScrollBarThumbMinSize();
+      if (interButtonLength < thumbMinWidth)
+      {
+         /**
+          * Scrollbar is too narrow for any thumb to be drawn
+          */
+         return;
+      }
+
+      final int modelWidth = model.getWidth();
+      final int modelPosition = model.getPosition();
+
+      /**
+       * We must treat zero-width case specially to prevent division by zero. 
+       */
+      final boolean isDegeneratedCase = modelWidth == 0;
+
+      /**
+       * Note: we calculate the gap between the thumb and the bottom button
+       * instead of thumb size. Otherwise we can get the 1-pixel gap between the
+       * thumb and the button at the end of thumb travel, which is visible to the
+       * user. We prefer to get slightly incorrect thumb size, which is hardly
+       * seen with the naked eye.
+       */
+      int thumbEnd = thumbEndLimit;
+      if (!isDegeneratedCase)
+      {
+         thumbEnd -= (modelWidth - model.getLastVisible()) * interButtonLength / modelWidth;
+      }
+
+      int thumbPosX = buttonLongDim;
+      if (!isDegeneratedCase)
+      {
+         thumbPosX += interButtonLength * modelPosition / modelWidth;
+      }
+
+      int thumbWidth = thumbEnd - thumbPosX;
+
+      /**
+       * Test if the thumb is too narrow, try to handle this
+       */
+      final int thumbInflateNeeded = thumbMinWidth - thumbWidth;
+      if (thumbInflateNeeded > 0)
+      {
+         /**
+          * The 'true' calculated thumb width is to small to be usable, so we try
+          * to increase it.
+          */
+         /**
+          * Note: the check above guarantees, that the inter-button space here is wide enough
+          * to contain a thumb of a minimum thumb width.
+          */
+         thumbWidth = thumbMinWidth;
+         final int halfIncrease = thumbInflateNeeded / 2;
+         thumbPosX = Math.min(Math.max(thumbPosX - halfIncrease, 0), thumbEndLimit - thumbWidth);
+      }
+
+      final int thumbSize = theme.getScrollBarThumbWidth();
+
+      final boolean thumbPressed = lastPressedPart == Part.THUMB;
+      final boolean thumbHover = lastHoverPart == Part.THUMB;
+      final int thumbPosY = (dim.height - thumbSize) / 2;
+
+      theme.drawScrollBarThumb(gd, gc, thumbPosX, thumbPosY, thumbWidth, thumbSize, thumbPressed,
+               thumbHover, true);
+   }
+
+   @Override
+   public Dimension minimumSize()
+   {
+      CoordinatesConversion cc = screen().coordinates();
+      return new Dimension(cc.columnFromPixels(minLength()), cc.rowFromPixels(minWidth()));
+   }
+
+   @Override
+   public void mouseDragged(/* @Nullable */ MouseEvent e)
+   {
+      if (lastPressedPart != Part.THUMB)
+      {
+         /**
+          * Effectively disallow any other mouse operation after the
+          * mouse pointer was dragged not by the thumb. Allowing it
+          * may look a bit weird from the user stand point.
+          */
+         lastPressedPart = Part.NONE;
+         return;
+      }
+
+      assert e != null;
+
+      final NativeDimension dim = physicalDimension();
+      final int scrollBarWidth = dim.width;
+      final Theme theme = ThemeManager.getCurrentTheme();
+
+      final int buttonLongDim = Math.min(scrollBarWidth / 2, theme.getScrollBarButtonHeight());
+
+      /**
+       * The length between the left and right buttons
+       */
+      final int interButtonLength = scrollBarWidth - 2 * buttonLongDim;
+
+      /**
+       * Check for the case the widget width is not enough for two
+       * buttons and minimum thumb.
+       *
+       * FIXME: in this situation the drag start should nod be possible,
+       * so it is better replace the test by an assertion.
+       */
+      final int thumbMinWidth = theme.getScrollBarThumbMinSize();
+      if (interButtonLength < thumbMinWidth)
+      {
+         /**
+          * FIXME: in this situation the drag start should nod be
+          * possible, so it is better replace the return statement by
+          * an assertion.
+          */
+         return;
+      }
+
+      /**
+       * Note: we calculate the gap between the thumb and the right
+       * button instead of thumb size. Otherwise we can get the
+       * 1-pixel gap between the thumb and the button at the end of
+       * thumb travel, which is visible to the user. We prefer to get
+       * slightly incorrect thumb size, which is hardly seen with the
+       * naked eye.
+       */
+      final int modelPage = model.getPage();
+      final int modelWidth = model.getWidth();
+      final int modelPosition = model.getPosition();
+
+      /**
+       * We must treat zero-width case specially to prevent division by zero. 
+       */
+      final boolean isDegeneratedCase = modelWidth == 0;
+
+      final int thumbEndLimit = scrollBarWidth - buttonLongDim;
+      int thumbEnd = thumbEndLimit;
+      if (!isDegeneratedCase)
+      {
+         thumbEnd -= (modelWidth - modelPage - modelPosition) * interButtonLength / modelWidth;
+      }
+
+      int thumbPosX = buttonLongDim;
+      if (!isDegeneratedCase)
+      {
+         thumbPosX += interButtonLength * modelPosition / modelWidth;
+      }
+
+      int thumbWidth = thumbEnd - thumbPosX;
+
+      /**
+       * Test if the thumb is too narrow, try to handle this
+       */
+      final int thumbInflateNeeded = thumbMinWidth - thumbWidth;
+      if (thumbInflateNeeded > 0)
+      {
+         /**
+          * The 'true' calculated thumb width is to small to be usable, so we try
+          * to increase it.
+          */
+         /**
+          * Note: the check above guarantees, that the inter-button space here is wide enough
+          * to contain a thumb of a minimum thumb width.
+          */
+         thumbWidth = thumbMinWidth;
+      }
+
+      /**
+       * Rightmost horizontal position
+       */
+      int maxPosX = interButtonLength - thumbWidth;
+      int maxModelPosition = modelWidth - modelPage;
+      int offset = e.getX() - lastMousePos;
+      int newModelPosition = maxPosX > 0 ? lastModelPos + offset * maxModelPosition / maxPosX : 0;
+      model.setPosition(Math.min(maxModelPosition, Math.max(0, newModelPosition)));
+   }
+
+   /**
+    * Update the {@link #lastHoverPart} and {@link #highLightedRect} field values
+    * when the mouse position changes.
+    * 
+    * @param x
+    *        the X coordinate
+    * @param y
+    *        the Y coordinate
+    * @return see above.
+    */
+   @Override
+   protected void updateMouseHitInfo(int x, int y)
+   {
+      NativeDimension dim = physicalDimension();
+      NativePoint p = screenPhysicalLocation();
+      int localX = x - p.x;
+
+      int scrollBarWidth = dim.width;
+      if (localX < 0 || localX >= scrollBarWidth)
+      {
+         /**
+          * Mouse is out of widget horizontal bounds
+          */
+         lastHoverPart = Part.NONE;
+         highLightedRect = null;
+         return;
+      }
+
+      Theme theme = ThemeManager.getCurrentTheme();
+      int buttonLongDim = Math.min(scrollBarWidth / 2, theme.getScrollBarButtonHeight());
+
+      /**
+       * The length between the left and right buttons
+       */
+      int interButtonLength = scrollBarWidth - 2 * buttonLongDim;
+
+      /**
+       * Check for the case the widget width is not enough for just two buttons.
+       * In this case the buttons should not be visible, and the scrollbar is unusable.
+       */
+      if (interButtonLength < 0)
+      {
+         lastHoverPart = Part.NONE;
+         highLightedRect = null;
+         return;
+      }
+
+      // compute buttons size
+
+      int buttonShortDim = theme.getScrollBarButtonWidth();
+
+      int rectTop = (dim.height - buttonShortDim) / 2;
+
+      int localY = y - p.y;
+      if ((localY < rectTop) || (localY >= (rectTop + buttonShortDim)))
+      {
+         lastHoverPart = Part.NONE;
+         highLightedRect = null;
+         return;
+      }
+
+      /**
+       * Now the pointer is in the widget active area.
+       */
+      int rectLeft;
+      int rectRight;
+
+      if (localX < buttonLongDim)
+      {
+         // Left button
+         rectLeft = 0;
+         rectRight = buttonLongDim;
+         lastHoverPart = Part.LEFT_BUTTON;
+      }
+      else
+      {
+         final int thumbEndLimit = scrollBarWidth - buttonLongDim;
+         if (localX >= thumbEndLimit)
+         {
+            // Right button
+            rectLeft = thumbEndLimit;
+            rectRight = scrollBarWidth;
+            lastHoverPart = Part.RIGHT_BUTTON;
+         }
+         else
+         {
+            /**
+             * Pointer is between the buttons.
+             */
+            final int thumbMinWidth = theme.getScrollBarThumbMinSize();
+            if (interButtonLength < thumbMinWidth)
+            {
+               /**
+                * Scrollbar is too narrow for any thumb to be drawn
+                */
+               lastHoverPart = Part.NONE;
+               highLightedRect = null;
+               return;
+            }
+
+            int modelWidth = model.getWidth();
+            int modelPosition = model.getPosition();
+
+            /**
+             * We must treat zero-width case specially to prevent division by zero. 
+             */
+            boolean isDegeneratedCase = modelWidth == 0;
+
+            /**
+             * Note: we calculate the gap between the thumb and the bottom button
+             * instead of thumb size. Otherwise we can get the 1-pixel gap between the
+             * thumb and the button at the end of thumb travel, which is visible to the
+             * user. We prefer to get slightly incorrect thumb size, which is hardly
+             * seen with the naked eye.
+             */
+            int thumbEnd = thumbEndLimit;
+            if (!isDegeneratedCase)
+            {
+               thumbEnd -= (modelWidth - model.getLastVisible()) * interButtonLength / modelWidth;
+            }
+
+            int thumbPosX = buttonLongDim;
+            if (!isDegeneratedCase)
+            {
+               thumbPosX += interButtonLength * modelPosition / modelWidth;
+            }
+
+            int thumbWidth = thumbEnd - thumbPosX;
+
+            /**
+             * Test if the thumb is too narrow, try to handle this
+             */
+            final int thumbInflateNeeded = thumbMinWidth - thumbWidth;
+            if (thumbInflateNeeded > 0)
+            {
+               /**
+                * The 'true' calculated thumb width is to small to be usable, so we try
+                * to increase it.
+                */
+               /**
+                * Note: the check above guarantees, that the inter-button space here is wide enough
+                * to contain a thumb of a minimum thumb width.
+                */
+               thumbWidth = thumbMinWidth;
+               final int halfIncrease = thumbInflateNeeded / 2;
+               thumbPosX = Math.min(Math.max(thumbPosX - halfIncrease, 0),
+                        thumbEndLimit - thumbWidth);
+            }
+
+            if (localX < thumbPosX)
+            {
+               // Left area
+               rectLeft = buttonLongDim;
+               rectRight = thumbPosX;
+               lastHoverPart = Part.LEFT_AREA;
+            }
+            else if (localX < thumbPosX + thumbWidth)
+            {
+               // Thumb
+               rectLeft = thumbPosX;
+               rectRight = thumbPosX + thumbWidth;
+               lastHoverPart = Part.THUMB;
+            }
+            else
+            {
+               // Right area
+               rectLeft = thumbEndLimit;
+               rectRight = scrollBarWidth;
+               lastHoverPart = Part.RIGHT_AREA;
+            }
+         }
+      }
+
+      highLightedRect = new NativeRectangle(rectTop, rectLeft, rectTop + buttonShortDim,
+               rectRight).translate(p);
+   }
+
+   @Override
+   public void mousePressed(/* @Nullable */ MouseEvent e)
+   {
+      assert e != null;
+      final int x = e.getX();
+      final int y = e.getY();
+      lastMousePos = x;
+      lastModelPos = model.getPosition();
+      updateMouseHitInfo(x, y);
+      if (lastPressedPart != lastHoverPart)
+      {
+         lastPressedPart = lastHoverPart;
+         // FIXME: repaint rectangle instead of whole widget area
+         repaint();
+      }
+   }
+}

=== added file 'src/com/goldencode/p2j/ui/client/widget/LightweightWidget.java'
--- src/com/goldencode/p2j/ui/client/widget/LightweightWidget.java	1970-01-01 00:00:00 +0000
+++ src/com/goldencode/p2j/ui/client/widget/LightweightWidget.java	2021-06-09 19:11:07 +0000
@@ -0,0 +1,34 @@
+/**
+ * 
+ */
+package com.goldencode.p2j.ui.client.widget;
+
+import com.goldencode.p2j.ui.client.*;
+import com.goldencode.p2j.ui.client.gui.driver.*;
+
+/**
+ * This class provides default implementations for a few {@link AbstractWidget} methods.
+ */
+abstract public class LightweightWidget extends AbstractWidget<GuiOutputManager>
+{
+
+   /**
+    * Default constructor.
+    */
+   public LightweightWidget()
+   {
+   }
+
+   @Override
+   public Dimension dimension()
+   {
+      return new Dimension(width(), height());
+   }
+
+   @Override
+   public Dimension minimumSize()
+   {
+      return dimension();
+   }
+
+}
\ No newline at end of file

=== added file 'src/com/goldencode/p2j/ui/client/widget/ScrollableContainer.java'
--- src/com/goldencode/p2j/ui/client/widget/ScrollableContainer.java	1970-01-01 00:00:00 +0000
+++ src/com/goldencode/p2j/ui/client/widget/ScrollableContainer.java	2021-06-10 18:42:08 +0000
@@ -0,0 +1,183 @@
+package com.goldencode.p2j.ui.client.widget;
+
+import com.goldencode.p2j.ui.*;
+import com.goldencode.p2j.ui.client.*;
+import com.goldencode.p2j.ui.client.gui.driver.*;
+import com.goldencode.p2j.ui.client.model.*;
+
+/**
+ * This container widget wraps a child widget with two scrollbars.
+ *
+ */
+public class ScrollableContainer extends GenericContainer
+{
+   /**
+    * A widget to paint the corner between the scrollbars.
+    */
+   private GenericWidget corner = new GenericWidget()
+   {
+      @Override
+      public void draw()
+      {
+         NativePoint p = physicalLocation();
+         NativeDimension d = physicalDimension();
+         GuiDriver<?, ?> gd = (GuiDriver<?, ?>) OutputManager.getDriver();
+
+         gd.setColor(gd.getSysColor(SysColor.COLOR_SCROLLBAR));
+         gd.fillRect(p.x, p.y, d.width, d.height);
+      }
+   };
+
+   /**
+    * The horizontal scrollbar.
+    */
+   private AbstractScrollBar hBar;
+
+   /**
+    * The horizontal scrolling model.
+    */
+   private ScrollModel hModel;
+
+   /**
+    * The vertical scrollbar.
+    */
+   private AbstractScrollBar vBar;
+
+   /**
+    * The vertical scrolling model.
+    */
+   private ScrollModel vModel;
+
+   /**
+    * The managed widget. Must implement {@link I2DScrollable} interface.
+    */
+   private AbstractWidget<GuiOutputManager> widget;
+
+   public ScrollableContainer(final AbstractWidget<GuiOutputManager> widget, final ScrollModel hModel,
+            final ScrollModel vModel)
+   {
+      assert widget instanceof I2DScrollable;
+
+      this.hModel = hModel;
+      this.vModel = vModel;
+      this.widget = widget;
+      add(widget);
+      hBar = new HScrollBar(hModel);
+      add(hBar);
+      vBar = new VScrollBar(vModel);
+      add(vBar);
+      add(corner);
+   }
+
+   @Override
+   public void draw()
+   {
+      final NativePoint p = physicalLocation();
+      final NativeDimension d = physicalDimension();
+
+      final NativeRectangle clip = clipRectangle(p, d);
+
+      GuiDriver<?, ?> gd = (GuiDriver<?, ?>) OutputManager.getDriver();
+      gd.draw(p, clip, () -> {
+         for (Widget<GuiOutputManager> w : widgets())
+         {
+            if (w.isVisible())
+            {
+               w.draw();
+            }
+         }
+      });
+   }
+
+   @Override
+   public void doLayout()
+   {
+      final double containerHeight = height();
+      final double containerWidth = width();
+
+      final double hbHeight = hBar.minimumSize().height();
+      final double vbWidth = vBar.minimumSize().width();
+      corner.setSize(vbWidth, hbHeight);
+      corner.setLocation(containerWidth - vbWidth, containerHeight - hbHeight);
+
+      while (true)
+      {
+         double widgetWidth = containerWidth;
+         double widgetHeight = containerHeight;
+
+         final boolean hScrollAvailable = hModel.isScrollAvailable();
+         if (hScrollAvailable)
+         {
+            widgetHeight -= hbHeight;
+         }
+
+         final boolean vScrollAvailable = vModel.isScrollAvailable();
+         if (vScrollAvailable)
+         {
+            widgetWidth -= vbWidth;
+         }
+
+         if (hScrollAvailable)
+         {
+            final BaseConfig hbConfig = hBar.config();
+            hbConfig.widthChars = widgetWidth;
+            hbConfig.heightChars = hbHeight;
+            hBar.setLocation(0, widgetHeight);
+            hBar.show();
+         }
+         else
+         {
+            hBar.hide();
+         }
+
+         if (vScrollAvailable)
+         {
+            final BaseConfig vbConfig = vBar.config();
+            vbConfig.widthChars = vbWidth;
+            vbConfig.heightChars = widgetHeight;
+            vBar.setLocation(widgetWidth, 0);
+            vBar.show();
+         }
+         else
+         {
+            vBar.hide();
+         }
+
+         if (widget instanceof Container)
+         {
+            /**
+             * Note: this can lead to calling this doLayout() method recursively,
+             * this behavior is expected.
+             */
+            ((Container<?>) widget).setSize(widgetWidth, widgetHeight);
+         }
+         else
+         {
+            final BaseConfig widgetConfig = widget.config();
+            assert widgetConfig != null;
+            widgetConfig.widthChars = widgetWidth;
+            widgetConfig.heightChars = widgetHeight;
+         }
+
+         widget.setLocation(0, 0);
+
+         /**
+          * Note: we need to re-evaluate the scrollbar models availabilities here since they
+          * could have been changed in the above call to setSize(). 
+          */
+         corner.setVisible(hModel.isScrollAvailable() && vModel.isScrollAvailable());
+
+         I2DScrollable scrollableWidget = (I2DScrollable) widget;
+
+         /**
+          * Note: be sure both update procedures always called.
+          */
+         boolean changed = scrollableWidget.updateVScrollModel();
+         changed |= scrollableWidget.updateHScrollModel();
+         if (!changed)
+         {
+            return;
+         }
+      }
+   }
+}

=== added file 'src/com/goldencode/p2j/ui/client/widget/VScrollBar.java'
--- src/com/goldencode/p2j/ui/client/widget/VScrollBar.java	1970-01-01 00:00:00 +0000
+++ src/com/goldencode/p2j/ui/client/widget/VScrollBar.java	2021-06-09 17:35:01 +0000
@@ -0,0 +1,436 @@
+package com.goldencode.p2j.ui.client.widget;
+
+import java.awt.event.*;
+
+import com.goldencode.p2j.ui.*;
+import com.goldencode.p2j.ui.client.*;
+import com.goldencode.p2j.ui.client.gui.*;
+import com.goldencode.p2j.ui.client.gui.driver.*;
+import com.goldencode.p2j.ui.client.gui.theme.*;
+import com.goldencode.p2j.ui.client.model.*;
+
+public final class VScrollBar extends AbstractScrollBar
+{
+
+   /**
+    * The (only) constructor for this widget.
+    * 
+    * @param model the scrolling model.
+    */
+   public VScrollBar(ScrollModel model)
+   {
+      super(model);
+   }
+
+   @Override
+   protected void doDraw(GuiDriver<?, ?> gd, GuiColorResolver gc)
+   {
+      final NativeDimension dim = physicalDimension();
+      final int scrollBarHeight = dim.height;
+      final Theme theme = ThemeManager.getCurrentTheme();
+
+      // fill in scroll bar area
+      theme.drawScrollBarGauge(gd, gc, 0, 0, dim.width, scrollBarHeight,
+               false /* isHorizontal */);
+
+      final int buttonLongDim = Math.min(scrollBarHeight / 2, theme.getScrollBarButtonHeight());
+
+      /**
+       * The length between the lefttop and bottom buttons
+       */
+      final int interButtonLength = scrollBarHeight - 2 * buttonLongDim;
+
+      /**
+       * Check for the case the widget width is not enough for just two buttons.
+       * In this case neither buttons nor thumb should not be visible.
+       */
+      if (interButtonLength < 0)
+      {
+         return;
+      }
+
+      final int buttonShortDim = theme.getScrollBarButtonWidth();
+
+      /**
+       * We center the vertical scrollbar horizontally
+       */
+      final int buttonX = (dim.width - buttonShortDim) / 2;
+
+      // draw left/top button
+      ThemeManager.getCurrentTheme().drawScrollBarButton(gd, gc, buttonX, 0, buttonShortDim,
+               buttonLongDim, ScrollBar.Position.LEFT, false, true,
+               lastPressedPart == Part.LEFT_BUTTON, lastHoverPart == Part.LEFT_BUTTON);
+
+      // draw right/bottom button
+      ThemeManager.getCurrentTheme().drawScrollBarButton(gd, gc, buttonX,
+               scrollBarHeight - buttonLongDim, buttonShortDim, buttonLongDim,
+               ScrollBar.Position.RIGHT, false, true, lastPressedPart == Part.RIGHT_BUTTON,
+               lastHoverPart == Part.RIGHT_BUTTON);
+
+      final int thumbMinWidth = theme.getScrollBarThumbMinSize();
+      if (interButtonLength < thumbMinWidth)
+      {
+         /**
+          * Scrollbar is too narrow for any thumb to be drawn
+          */
+         return;
+      }
+
+      final int modelWidth = model.getWidth();
+      final int modelPosition = model.getPosition();
+
+      /**
+       * Note: we calculate the gap between the thumb and the bottom button
+       * instead of thumb size. Otherwise we can get the 1-pixel gap between the
+       * thumb and the button at the end of thumb travel, which is visible to the
+       * user. We prefer to get slightly incorrect thumb size, which is hardly
+       * seen with the naked eye.
+       */
+      /**
+       * We must treat zero-width case specially to prevent division by zero. 
+       */
+      final boolean isDegeneratedCase = modelWidth == 0;
+
+      final int thumbEndLimit = scrollBarHeight - buttonLongDim;
+      int thumbEnd = thumbEndLimit;
+      if (!isDegeneratedCase)
+      {
+         thumbEnd -= (modelWidth - model.getLastVisible()) * interButtonLength / modelWidth;
+      }
+
+      int thumbPosY = buttonLongDim;
+      if (!isDegeneratedCase)
+      {
+         thumbPosY += interButtonLength * modelPosition / modelWidth;
+      }
+
+      int thumbWidth = thumbEnd - thumbPosY;
+
+      /**
+       * Test if the thumb is too narrow, try to handle this
+       */
+      final int thumbInflateNeeded = thumbMinWidth - thumbWidth;
+      if (thumbInflateNeeded > 0)
+      {
+         /**
+          * The 'true' calculated thumb width is to small to be usable, so we try
+          * to increase it.
+          */
+         /**
+          * Note: the check above guarantees, that the inter-button space here is wide enough
+          * to contain a thumb of a minimum thumb width.
+          */
+         thumbWidth = thumbMinWidth;
+         final int halfIncrease = thumbInflateNeeded / 2;
+         thumbPosY = Math.min(Math.max(thumbPosY - halfIncrease, 0), thumbEndLimit - thumbWidth);
+      }
+
+      final int thumbSize = theme.getScrollBarThumbWidth();
+
+      final boolean thumbPressed = lastPressedPart == Part.THUMB;
+      final boolean thumbHover = lastHoverPart == Part.THUMB;
+
+      final int thumbPosX = (dim.width - thumbSize) / 2;
+
+      theme.drawScrollBarThumb(gd, gc, thumbPosX, thumbPosY, thumbSize, thumbWidth, thumbPressed,
+               thumbHover, false);
+   }
+
+   @Override
+   public Dimension minimumSize()
+   {
+      @SuppressWarnings("null")
+      CoordinatesConversion cc = screen().coordinates();
+      return new Dimension(cc.columnFromPixels(minWidth()), cc.rowFromPixels(minLength()));
+   }
+
+   @Override
+   public void mouseDragged(/* @Nullable */ MouseEvent e)
+   {
+      if (lastPressedPart != Part.THUMB)
+      {
+         /**
+          * Effectively disallow any other mouse operation after the
+          * mouse pointer was dragged not by the thumb. Allowing it
+          * may look a bit weird from the user stand point.
+          */
+         lastPressedPart = Part.NONE;
+         return;
+      }
+
+      assert e != null;
+
+      NativeDimension dim = physicalDimension();
+      int scrollBarHeight = dim.height;
+      Theme theme = ThemeManager.getCurrentTheme();
+
+      int buttonLongDim = Math.min(scrollBarHeight / 2, theme.getScrollBarButtonHeight());
+
+      /**
+       * The length between the left and right buttons
+       */
+      int interButtonLength = scrollBarHeight - 2 * buttonLongDim;
+
+      /**
+       * Check for the case the widget width is not enough for two
+       * buttons and minimum thumb.
+       *
+       */
+      final int thumbMinWidth = theme.getScrollBarThumbMinSize();
+      if (interButtonLength < thumbMinWidth)
+      {
+         /**
+          * FIXME: in this situation the drag start should nod be
+          * possible, so it is better replace the return statement by
+          * an assertion.
+          */
+         return;
+      }
+
+      /**
+       * Note: we calculate the gap between the thumb and the bottom button
+       * instead of thumb size. Otherwise we can get the 1-pixel gap between the
+       * thumb and the button at the end of thumb travel, which is visible to the
+       * user. We prefer to get slightly incorrect thumb size, which is hardly
+       * seen with the naked eye.
+       */
+      int modelPage = model.getPage();
+      int modelWidth = model.getWidth();
+      int modelPosition = model.getPosition();
+
+      /**
+       * We must treat zero-width case specially to prevent division by zero. 
+       */
+      boolean isDegeneratedCase = modelWidth == 0;
+
+      final int thumbEndLimit = scrollBarHeight - buttonLongDim;
+      int thumbEnd = thumbEndLimit;
+      if (!isDegeneratedCase)
+      {
+         thumbEnd -= (modelWidth - modelPage - modelPosition) * interButtonLength / modelWidth;
+      }
+
+      int thumbPosY = buttonLongDim;
+      if (!isDegeneratedCase)
+      {
+         thumbPosY += interButtonLength * modelPosition / modelWidth;
+      }
+
+      int thumbWidth = thumbEnd - thumbPosY;
+
+      /**
+       * Test if the thumb is too narrow, try to handle this
+       */
+      final int thumbInflateNeeded = thumbMinWidth - thumbWidth;
+      if (thumbInflateNeeded > 0)
+      {
+         /**
+          * The 'true' calculated thumb width is to small to be usable, so we try
+          * to increase it.
+          */
+         /**
+          * Note: the check above guarantees, that the inter-button space here is wide enough
+          * to contain a thumb of a minimum thumb width.
+          */
+         thumbWidth = thumbMinWidth;
+      }
+
+      /**
+       * Topmost vertical position
+       */
+      final int maxPosY = interButtonLength - thumbWidth;
+
+      final int maxModelPosition = modelWidth - modelPage;
+      final int offset = e.getY() - lastMousePos;
+      final int newModelPosition = maxPosY > 0 ? lastModelPos + offset * maxModelPosition / maxPosY : 0;
+      model.setPosition(Math.min(maxModelPosition, Math.max(0, newModelPosition)));
+   }
+
+   /**
+    * Update the {@link #lastHoverPart} and {@link #highLightedRect} field values
+    * when the mouse position changes.
+    * 
+    * @param x
+    *        the X coordinate
+    * @param y
+    *        the Y coordinate
+    * @return see above.
+    */
+   @Override
+   protected void updateMouseHitInfo(int x, int y)
+   {
+      NativeDimension dim = physicalDimension();
+      NativePoint p = screenPhysicalLocation();
+      int localY = y - p.y;
+
+      int scrollBarHeight = dim.height;
+      if (localY < 0 || localY >= scrollBarHeight)
+      {
+         /**
+          * Mouse is out of widget vertical bounds
+          */
+         lastHoverPart = Part.NONE;
+         highLightedRect = null;
+         return;
+      }
+
+      Theme theme = ThemeManager.getCurrentTheme();
+      int buttonLongDim = Math.min(scrollBarHeight / 2, theme.getScrollBarButtonHeight());
+
+      /**
+       * The length between the left and right buttons
+       */
+      int interButtonLength = scrollBarHeight - 2 * buttonLongDim;
+
+      /**
+       * Check for the case the widget width is not enough for just two buttons.
+       * In this case the buttons should not be visible, and the scrollbar is unusable.
+       */
+      if (interButtonLength < 0)
+      {
+         lastHoverPart = Part.NONE;
+         highLightedRect = null;
+         return;
+      }
+
+      // compute buttons size
+
+      int buttonShortDim = theme.getScrollBarButtonWidth();
+
+      int rectLeft = (dim.width - buttonShortDim) / 2;
+
+      int localX = x - p.x;
+      if ((localX < rectLeft) || (localX >= (rectLeft + buttonShortDim)))
+      {
+         lastHoverPart = Part.NONE;
+         highLightedRect = null;
+         return;
+      }
+
+      /**
+       * Now the pointer is in the widget active area.
+       */
+      int rectTop;
+      int rectBottom;
+
+      if (localY < buttonLongDim)
+      {
+         // Top button
+         rectTop = 0;
+         rectBottom = buttonLongDim;
+         lastHoverPart = Part.LEFT_BUTTON;
+      }
+      else
+      {
+         final int thumbEndLimit = scrollBarHeight - buttonLongDim;
+         if (localY >= thumbEndLimit)
+         {
+            // Bottom button
+            rectTop = thumbEndLimit;
+            rectBottom = scrollBarHeight;
+            lastHoverPart = Part.RIGHT_BUTTON;
+         }
+         else
+         {
+            /**
+             * Pointer is between the buttons.
+             */
+            final int thumbMinWidth = theme.getScrollBarThumbMinSize();
+            if (interButtonLength < thumbMinWidth)
+            {
+               /**
+                * Scrollbar is too narrow for any thumb to be drawn
+                */
+               lastHoverPart = Part.NONE;
+               highLightedRect = null;
+               return;
+            }
+
+            int modelWidth = model.getWidth();
+            int modelPosition = model.getPosition();
+
+            /**
+             * We must treat zero-width case specially to prevent division by zero. 
+             */
+            boolean isDegeneratedCase = modelWidth == 0;
+
+            int thumbEnd = thumbEndLimit;
+            if (!isDegeneratedCase)
+            {
+               thumbEnd -= (modelWidth - model.getLastVisible()) * interButtonLength / modelWidth;
+            }
+
+            int thumbPosY = buttonLongDim;
+            if (!isDegeneratedCase)
+            {
+               thumbPosY += interButtonLength * modelPosition / modelWidth;
+            }
+
+            int thumbWidth = thumbEnd - thumbPosY;
+
+            /**
+             * Test if the thumb is too narrow, try to handle this
+             */
+            final int thumbInflateNeeded = thumbMinWidth - thumbWidth;
+            if (thumbInflateNeeded > 0)
+            {
+               /**
+                * The 'true' calculated thumb width is to small to be usable, so we try
+                * to increase it.
+                */
+               /**
+                * Note: the check above guarantees, that the inter-button space here is wide enough
+                * to contain a thumb of a minimum thumb width.
+                */
+               thumbWidth = thumbMinWidth;
+               final int halfIncrease = thumbInflateNeeded / 2;
+               thumbPosY = Math.min(Math.max(thumbPosY - halfIncrease, 0),
+                        thumbEndLimit - thumbWidth);
+            }
+
+            if (localY < thumbPosY)
+            {
+               // Top area
+               rectTop = buttonLongDim;
+               rectBottom = thumbPosY;
+               lastHoverPart = Part.LEFT_AREA;
+            }
+            else if (localY < thumbPosY + thumbWidth)
+            {
+               // Thumb
+               rectTop = thumbPosY;
+               rectBottom = thumbPosY + thumbWidth;
+               lastHoverPart = Part.THUMB;
+            }
+            else
+            {
+               // Bottom area
+               rectTop = thumbEndLimit;
+               rectBottom = scrollBarHeight;
+               lastHoverPart = Part.RIGHT_AREA;
+            }
+         }
+      }
+
+      highLightedRect = new NativeRectangle(rectLeft, rectTop, rectLeft + buttonShortDim,
+               rectBottom).translate(p);
+   }
+
+   @Override
+   public void mousePressed(/* @Nullable */ MouseEvent e)
+   {
+      assert e != null;
+      final int x = e.getX();
+      final int y = e.getY();
+      lastMousePos = y;
+      lastModelPos = model.getPosition();
+      updateMouseHitInfo(x, y);
+      if (lastPressedPart != lastHoverPart)
+      {
+         lastPressedPart = lastHoverPart;
+         // FIXME: repaint rectangle instead of whole widget area
+         repaint();
+      }
+   }
+
+}

=== modified file 'src/com/goldencode/p2j/ui/ocx/TreeViewNodeObject.java'
--- src/com/goldencode/p2j/ui/ocx/TreeViewNodeObject.java	2020-02-23 22:22:28 +0000
+++ src/com/goldencode/p2j/ui/ocx/TreeViewNodeObject.java	2021-06-16 22:43:05 +0000
@@ -78,7 +78,7 @@
 extends ComObject
 {
    /** TreeView node */
-   private TreeViewNodeResource node = null;
+   private TreeNodeResource node = null;
    
    /**
     * Constructor.
@@ -86,7 +86,7 @@
     * @param   node
     *          Node entry.
     */
-   private TreeViewNodeObject(TreeViewNodeResource node)
+   private TreeViewNodeObject(TreeNodeResource node)
    {
       this.node = node;
    }
@@ -99,7 +99,7 @@
     *          
     * @return a new instance wrapped as a comhandle.
     */
-   public static comhandle newInstance(TreeViewNodeResource node)
+   public static comhandle newInstance(TreeNodeResource node)
    {
       return node == null ? new comhandle() : new comhandle(new TreeViewNodeObject(node));
    }

=== modified file 'src/com/goldencode/p2j/util/handle.java'
--- src/com/goldencode/p2j/util/handle.java	2021-06-15 16:00:01 +0000
+++ src/com/goldencode/p2j/util/handle.java	2021-06-17 17:43:13 +0000
@@ -3433,7 +3433,7 @@
     *
     * @return   See above.
     */
-   public TreeNodeCollection<?> unwrapTreeNodeCollection()
+   public TreeNodeCollection unwrapTreeNodeCollection()
    {
       return unwrapImpl(this, TreeNodeCollection.class);
    }

=== added file 'src/com/goldencode/util/AbstractPrintable.java'
--- src/com/goldencode/util/AbstractPrintable.java	1970-01-01 00:00:00 +0000
+++ src/com/goldencode/util/AbstractPrintable.java	2021-06-12 17:10:47 +0000
@@ -0,0 +1,60 @@
+package com.goldencode.util;
+
+import com.google.common.base.*;
+import com.google.common.base.MoreObjects.*;
+
+/**
+ * {@link AbstractPrintable} provides a convenient ancestor for classes willing
+ * to implement the {@link #toString()} method using {@link ToStringHelper}.
+ * 
+ * Subclasses are supposed to redefine the
+ * {@link #updateStringHelper(ToStringHelper)} method.
+ * 
+ */
+public class AbstractPrintable
+{
+   /**
+   * Add class-specific fields to the helper.
+   * 
+   * The subclasses are supposed to override this method to add their
+   * own field info.
+   * 
+   * The default implementation does nothing.
+   *  
+   * @param stringHelper the string helper to update
+   */
+   protected void updateStringHelper(final ToStringHelper stringHelper)
+   {
+      // no-op
+   }
+
+   /**
+    * Returns a string representation of the object. In general, the
+    * {@code toString} method returns a string that
+    * "textually represents" this object. The result should
+    * be a concise but informative representation that is easy for a
+    * person to read.
+    * It is recommended that all subclasses override this method.
+    * <p>
+    * The {@code toString} method for class {@code Object}
+    * returns a string consisting of the name of the class of which the
+    * object is an instance, the at-sign character `{@code @}', and
+    * the unsigned hexadecimal representation of the hash code of the
+    * object. In other words, this method returns a string equal to the
+    * value of:
+    * <blockquote>
+    * <pre>
+    * getClass().getName() + '@' + Integer.toHexString(hashCode())
+    * </pre></blockquote>
+    *
+    * @return  a string representation of the object.
+    */
+   @SuppressWarnings("null")
+   @Override
+   public final String toString()
+   {
+      final ToStringHelper stringHelper = MoreObjects.toStringHelper(this);
+      updateStringHelper(stringHelper);
+      return stringHelper.toString();
+   }
+}

