You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@sis.apache.org by de...@apache.org on 2020/01/24 19:17:49 UTC

[sis] branch geoapi-4.0 updated (a70927e -> ee71343)

This is an automated email from the ASF dual-hosted git repository.

desruisseaux pushed a change to branch geoapi-4.0
in repository https://gitbox.apache.org/repos/asf/sis.git.


    from a70927e  Fix numerous problems in the initial GridView implementation (horizontal scroll bar was not visible, too many GridCell nodes were created, etc.)
     new 60f9fee  Fix broken link.
     new ee71343  A little bit of formatting improvements.

The 2 revisions listed above as "new" are entirely new to this
repository and will be described in separate emails.  The revisions
listed as "add" were already present in the repository and have only
been added to this reference.


Summary of changes:
 .../java/org/apache/sis/gui/coverage/GridRow.java  |   7 +
 .../org/apache/sis/gui/coverage/GridRowSkin.java   |  39 +++--
 .../java/org/apache/sis/gui/coverage/GridView.java | 177 +++++++++++++++++----
 .../org/apache/sis/gui/coverage/GridViewSkin.java  |  96 ++++++++++-
 .../main/java/org/apache/sis/io/wkt/WKTFormat.java |   2 +-
 5 files changed, 274 insertions(+), 47 deletions(-)


[sis] 01/02: Fix broken link.

Posted by de...@apache.org.
This is an automated email from the ASF dual-hosted git repository.

desruisseaux pushed a commit to branch geoapi-4.0
in repository https://gitbox.apache.org/repos/asf/sis.git

commit 60f9fee6b27c40c0570411f042f62bd648803714
Author: Martin Desruisseaux <ma...@geomatys.com>
AuthorDate: Fri Jan 24 19:41:44 2020 +0100

    Fix broken link.
---
 core/sis-referencing/src/main/java/org/apache/sis/io/wkt/WKTFormat.java | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/core/sis-referencing/src/main/java/org/apache/sis/io/wkt/WKTFormat.java b/core/sis-referencing/src/main/java/org/apache/sis/io/wkt/WKTFormat.java
index e5897d1..b591022 100644
--- a/core/sis-referencing/src/main/java/org/apache/sis/io/wkt/WKTFormat.java
+++ b/core/sis-referencing/src/main/java/org/apache/sis/io/wkt/WKTFormat.java
@@ -51,7 +51,7 @@ import org.apache.sis.internal.referencing.ReferencingFactoryContainer;
 
 /**
  * Parser and formatter for <cite>Well Known Text</cite> (WKT) strings.
- * This format handles a pair of {@link Parser} and {@link Formatter},
+ * This format handles a pair of {@link org.apache.sis.io.wkt.Parser} and {@link Formatter},
  * used by the {@code parse(…)} and {@code format(…)} methods respectively.
  * {@code WKTFormat} objects allow the following configuration:
  *


[sis] 02/02: A little bit of formatting improvements.

Posted by de...@apache.org.
This is an automated email from the ASF dual-hosted git repository.

desruisseaux pushed a commit to branch geoapi-4.0
in repository https://gitbox.apache.org/repos/asf/sis.git

commit ee71343d8196366d507f36d7eea217be9953df5c
Author: Martin Desruisseaux <ma...@geomatys.com>
AuthorDate: Fri Jan 24 20:12:34 2020 +0100

    A little bit of formatting improvements.
---
 .../java/org/apache/sis/gui/coverage/GridRow.java  |   7 +
 .../org/apache/sis/gui/coverage/GridRowSkin.java   |  39 +++--
 .../java/org/apache/sis/gui/coverage/GridView.java | 177 +++++++++++++++++----
 .../org/apache/sis/gui/coverage/GridViewSkin.java  |  96 ++++++++++-
 4 files changed, 273 insertions(+), 46 deletions(-)

diff --git a/application/sis-javafx/src/main/java/org/apache/sis/gui/coverage/GridRow.java b/application/sis-javafx/src/main/java/org/apache/sis/gui/coverage/GridRow.java
index 9b17b0b..ad89f85 100644
--- a/application/sis-javafx/src/main/java/org/apache/sis/gui/coverage/GridRow.java
+++ b/application/sis-javafx/src/main/java/org/apache/sis/gui/coverage/GridRow.java
@@ -20,6 +20,8 @@ import java.awt.image.RenderedImage;
 import javafx.scene.control.IndexedCell;
 import javafx.scene.control.Skin;
 import javafx.scene.control.skin.VirtualFlow;
+import javafx.scene.text.Font;
+import javafx.scene.text.FontWeight;
 
 
 /**
@@ -81,6 +83,7 @@ final class GridRow extends IndexedCell<Void> {
         flow = (GridViewSkin.Flow) owner;
         view = (GridView) owner.getParent();
         setPrefWidth(view.getContentWidth());
+        setFont(Font.font(null, FontWeight.BOLD, -1));      // Apply only to the header column.
     }
 
     /**
@@ -95,6 +98,10 @@ final class GridRow extends IndexedCell<Void> {
         super.updateIndex(row);
         y = view.toImageY(row);
         tileRow = view.toTileRow(row);
+        final Skin<?> skin = getSkin();
+        if (skin != null) {
+            ((GridRowSkin) skin).setRowIndex(row);
+        }
     }
 
     /**
diff --git a/application/sis-javafx/src/main/java/org/apache/sis/gui/coverage/GridRowSkin.java b/application/sis-javafx/src/main/java/org/apache/sis/gui/coverage/GridRowSkin.java
index bd54c0f..b085114 100644
--- a/application/sis-javafx/src/main/java/org/apache/sis/gui/coverage/GridRowSkin.java
+++ b/application/sis-javafx/src/main/java/org/apache/sis/gui/coverage/GridRowSkin.java
@@ -19,9 +19,10 @@ package org.apache.sis.gui.coverage;
 import java.util.List;
 import java.util.ArrayList;
 import javafx.scene.Node;
+import javafx.scene.text.Text;
 import javafx.scene.control.skin.CellSkinBase;
 import javafx.collections.ObservableList;
-import javafx.scene.text.Text;
+import javafx.geometry.Pos;
 
 
 /**
@@ -41,6 +42,15 @@ final class GridRowSkin extends CellSkinBase<GridRow> {
      */
     GridRowSkin(final GridRow owner) {
         super(owner);
+        setRowIndex(owner.getIndex());
+    }
+
+    /**
+     * Invoked when the index to show in the header column changed.
+     */
+    final void setRowIndex(final int index) {
+        final Text header = (Text) getChildren().get(0);
+        header.setText(getSkinnable().view.formatHeaderValue(index));
     }
 
     /**
@@ -69,9 +79,6 @@ final class GridRowSkin extends CellSkinBase<GridRow> {
          * The first child is a javafx.scene.text.Text instance, which we use for row header.
          */
         final GridRow row = getSkinnable();
-        final ObservableList<Node> children = getChildren();
-        final Text header = (Text) children.get(0);
-        header.setText(String.valueOf(row.getIndex()));     // TODO: format
         /*
          * Get the beginning (pos) and end (limit) of the region to render. We create only the amount
          * of GridCell instances needed for rendering this region. We should not create cells for the
@@ -83,13 +90,24 @@ final class GridRowSkin extends CellSkinBase<GridRow> {
         final double cellWidth   = row.view.cellWidth.get();            // Includes the cell spacing.
         final double cellSpacing = row.view.cellSpacing.get();
         final double available   = cellWidth - cellSpacing;
-        double pos = row.flow.getHorizontalPosition();
-        final double limit = pos + row.flow.getWidth();
-        int column = (int) (pos / cellWidth);
+        double pos = row.flow.getHorizontalPosition();                  // Horizontal position in the virtual view.
+        final double limit = pos + row.flow.getWidth();                 // Horizontal position where to stop.
+        int column = (int) (pos / cellWidth);                           // Column index in the RenderedImage.
+        /*
+         * Set the position of the header cell, but not its content. The content has been set by
+         * `setRowIndex(int)` and does not need to be recomputed even during horizontal scroll.
+         */
+        final ObservableList<Node> children = getChildren();
+        final Text header = (Text) children.get(0);
+        header.resizeRelocate(pos + cellSpacing, y, headerWidth - cellSpacing, height);
+        pos += headerWidth;
+        /*
+         * For sample value, we need to recompute both the values and the position. Note that even if
+         * the cells appear at the same positions visually (with different content), they moved in the
+         * virtual flow if some scrolling occurred.
+         */
         int childIndex = 0;
         List<GridCell> newChildren = null;
-        header.resizeRelocate(pos, y, headerWidth, height);
-        pos += headerWidth;
         final int count = children.size();
         while (pos < limit) {
             final GridCell cell;
@@ -97,6 +115,7 @@ final class GridRowSkin extends CellSkinBase<GridRow> {
                 cell = (GridCell) children.get(childIndex);
             } else {
                 cell = new GridCell();
+                cell.setAlignment(Pos.CENTER_RIGHT);
                 if (newChildren == null) {
                     newChildren = new ArrayList<>(1 + (int) ((limit - pos) / cellWidth));
                 }
@@ -104,7 +123,7 @@ final class GridRowSkin extends CellSkinBase<GridRow> {
             }
             final String value = row.getSampleValue(column++);
             cell.updateItem(value, value == GridView.OUT_OF_BOUNDS);            // Identity comparison is okay here.
-            cell.resizeRelocate(pos + cellSpacing, 0, available, height);
+            cell.resizeRelocate(pos, 0, available, height);
             pos += cellWidth;
         }
         /*
diff --git a/application/sis-javafx/src/main/java/org/apache/sis/gui/coverage/GridView.java b/application/sis-javafx/src/main/java/org/apache/sis/gui/coverage/GridView.java
index 598b169..e7b9b6f 100644
--- a/application/sis-javafx/src/main/java/org/apache/sis/gui/coverage/GridView.java
+++ b/application/sis-javafx/src/main/java/org/apache/sis/gui/coverage/GridView.java
@@ -17,9 +17,12 @@
 package org.apache.sis.gui.coverage;
 
 import java.util.Arrays;
+import java.text.NumberFormat;
+import java.text.FieldPosition;
 import java.awt.image.Raster;
 import java.awt.image.RenderedImage;
 import java.awt.image.SampleModel;
+import java.awt.image.DataBuffer;
 import javafx.beans.DefaultProperty;
 import javafx.beans.property.DoubleProperty;
 import javafx.beans.property.IntegerProperty;
@@ -30,6 +33,8 @@ import javafx.beans.property.SimpleObjectProperty;
 import javafx.beans.value.ObservableValue;
 import javafx.scene.control.Control;
 import javafx.scene.control.Skin;
+import javafx.scene.paint.Color;
+import javafx.scene.paint.Paint;
 import org.apache.sis.util.ArgumentChecks;
 import org.apache.sis.coverage.grid.GridCoverage;
 
@@ -84,9 +89,10 @@ public class GridView extends Control {
     private int tileGridXOffset, tileGridYOffset;
 
     /**
-     * The {@link #imageProperty} tiles, fetched when first needed. All {@code Raster[]} array element and
-     * {@code Raster} sub-array elements are initially {@code null} and created when first needed.
-     * This field is null if and only if the image is null.
+     * The {@link #imageProperty} tiles (fetched when first needed), or {@code null} if the image is null.
+     * All {@code Raster[]} array element and {@code Raster} sub-array elements are initially {@code null}
+     * and initialized when first needed. We store {@link RenderedImage#getTile(int, int)} results because
+     * we do not know if calling that method causes a costly computation or not (it depends on image class).
      */
     private Raster[][] tiles;
 
@@ -140,22 +146,72 @@ public class GridView extends Control {
     public final DoubleProperty cellSpacing;
 
     /**
+     * The background color of row and column headers.
+     *
+     * <p>We do not define getter/setter for this property; use {@link ObjectProperty#set(Object)}
+     * directly instead. We omit the "Property" suffix for making this operation more natural.</p>
+     */
+    public final ObjectProperty<Paint> headerBackground;
+
+    /**
+     * The formatter to use for writing header values (row and column numbers) or the sample values.
+     */
+    private final NumberFormat headerFormat, cellFormat;
+
+    /**
+     * Required when invoking {@link #cellFormat} methods but not used by this class.
+     * This argument is not allowed to be {@code null}, so we create an instance once
+     * and reuse it at each method call.
+     */
+    private final FieldPosition formatField;
+
+    /**
+     * A buffer for writing sample values with {@link #cellFormat}, reused for each value to format.
+     */
+    private final StringBuffer buffer;
+
+    /**
+     * The last value formatted by {@link #cellFormat}. We keep this information because it happens often
+     * that the same value is repeated for many cells, especially in area containing fill or missing values.
+     * If the value is the same, we will reuse the {@link #lastValueAsText}.
+     *
+     * <p>Note: the use of {@code double} is sufficient since rendered images can not store {@code long} values,
+     * so there is no precision lost that we could have with conversions from {@code long} to {@code double}.</p>
+     */
+    private double lastValue;
+
+    /**
+     * The formatting of {@link #lastValue}.
+     */
+    private String lastValueAsText;
+
+    /**
+     * Whether the sample values are integers.
+     */
+    private boolean isInteger;
+
+    /**
      * Creates an initially empty grid view. The content can be set after
      * construction by a call to {@link #setImage(RenderedImage)}.
      */
     public GridView() {
-        imageProperty = new SimpleObjectProperty<>(this, "image");
-        bandProperty  = new SimpleIntegerProperty (this, "band");
-        headerWidth   = new SimpleDoubleProperty  (this, "headerWidth", 60);
-        cellWidth     = new SimpleDoubleProperty  (this, "cellWidth",   60);
-        cellHeight    = new SimpleDoubleProperty  (this, "cellHeight",  20);
-        cellSpacing   = new SimpleDoubleProperty  (this, "cellSpacing",  2);
+        imageProperty    = new SimpleObjectProperty<>(this, "image");
+        bandProperty     = new SimpleIntegerProperty (this, "band");
+        headerWidth      = new SimpleDoubleProperty  (this, "headerWidth", 60);
+        cellWidth        = new SimpleDoubleProperty  (this, "cellWidth",   60);
+        cellHeight       = new SimpleDoubleProperty  (this, "cellHeight",  20);
+        cellSpacing      = new SimpleDoubleProperty  (this, "cellSpacing",  4);
+        headerBackground = new SimpleObjectProperty<>(this, "headerBackground", Color.GAINSBORO);
+        headerFormat     = NumberFormat.getIntegerInstance();
+        cellFormat       = NumberFormat.getInstance();
+        formatField      = new FieldPosition(0);
+        buffer           = new StringBuffer();
+        tileWidth        = 1;
+        tileHeight       = 1;       // For avoiding division by zero.
 
+        setMinSize(120, 40);        // 2 cells on each dimension.
         imageProperty.addListener(this::startImageLoading);
         // Other listeners registered by GridViewSkin.Flow.
-
-        tileWidth  = 1;
-        tileHeight = 1;     // For avoiding division by zero.
     }
 
     /**
@@ -205,8 +261,9 @@ public class GridView extends Control {
      */
     public final void setBand(final int index) {
         final RenderedImage image = getImage();
-        if (image != null) {
-            ArgumentChecks.ensureBetween("band", 0, image.getSampleModel().getNumBands() - 1, index);
+        final SampleModel sm;
+        if (image != null && (sm = image.getSampleModel()) != null) {
+            ArgumentChecks.ensureBetween("band", 0, sm.getNumBands() - 1, index);
         } else {
             ArgumentChecks.ensurePositive("band", index);
         }
@@ -226,9 +283,10 @@ public class GridView extends Control {
     private void startImageLoading(final ObservableValue<? extends RenderedImage> property,
                                    final RenderedImage previous, final RenderedImage image)
     {
-        tiles  = null;       // Let garbage collector dispose the rasters.
-        width  = 0;
-        height = 0;
+        tiles     = null;       // Let garbage collector dispose the rasters.
+        width     = 0;
+        height    = 0;
+        isInteger = false;
         if (image != null) {
             width           = image.getWidth();
             height          = image.getHeight();
@@ -242,14 +300,53 @@ public class GridView extends Control {
             tileGridYOffset = Math.toIntExact(((long) image.getTileGridYOffset()) - minY + ((long) tileHeight) * minTileY);
             numXTiles       = image.getNumXTiles();
             tiles           = new Raster[image.getNumYTiles()][];
-            final int n     = image.getSampleModel().getNumBands();
-            if (bandProperty.get() >= n) {
-                bandProperty.set(n - 1);
-            }
-            final Skin<?> skin = getSkin();
-            if (skin instanceof GridViewSkin) {
-                ((GridViewSkin) skin).updateItemCount();
+            final SampleModel sm = image.getSampleModel();
+            if (sm != null) {                               // Should never be null, but we are paranoiac.
+                final int numBands = sm.getNumBands();
+                if (bandProperty.get() >= numBands) {
+                    bandProperty.set(numBands - 1);
+                }
+                final int dataType = sm.getDataType();
+                isInteger = (dataType >= DataBuffer.TYPE_BYTE && dataType <= DataBuffer.TYPE_INT);
+                if (isInteger) {
+                    cellFormat.setMaximumFractionDigits(0);
+                } else {
+                    /*
+                     * TODO: compute the number of fraction digits from a "sampleResolution" image property
+                     * (of type float[] or double[]) if present. Provide a widget allowing user to set pattern.
+                     */
+                    cellFormat.setMinimumFractionDigits(1);
+                    cellFormat.setMaximumFractionDigits(1);
+                }
+                formatChanged(false);
             }
+            contentChanged(true);
+        }
+    }
+
+    /**
+     * Invoked when the content may have changed. If {@code all} is {@code true}, then everything
+     * may have changed including the number of rows and columns. If {@code all} is {@code false}
+     * then the number of rows and columns is assumed the same.
+     */
+    final void contentChanged(final boolean all) {
+        final Skin<?> skin = getSkin();             // May be null if the view is not yet shown.
+        if (skin instanceof GridViewSkin) {         // Could be a user instance (not recommended).
+            ((GridViewSkin) skin).contentChanged(all);
+        }
+    }
+
+    /**
+     * Invoked when the {@link #cellFormat} configuration changed.
+     *
+     * @param  notify  whether to notify the renderer about the change. Can be {@code false}
+     *                 if the renderer is going to be notified anyway by another method call.
+     */
+    private void formatChanged(final boolean notify) {
+        buffer.setLength(0);
+        lastValueAsText = cellFormat.format(lastValue, buffer, formatField).toString();
+        if (notify) {
+            contentChanged(false);
         }
     }
 
@@ -264,6 +361,8 @@ public class GridView extends Control {
     /**
      * Returns the number of rows in the image. This is also the number of rows in the
      * {@link GridViewSkin} virtual flow, which is using a vertical primary direction.
+     *
+     * @see javafx.scene.control.skin.VirtualContainerBase#getItemCount()
      */
     final int getImageHeight() {
         return height;
@@ -324,16 +423,36 @@ public class GridView extends Control {
             }
             final int x = Math.addExact(column, minX);
             final int b = getBand();
-            final Number value;
-            // TODO: also return Float or Integer.
-            value = tile.getSampleDouble(x, y, b);
-            // TODO: format.
-            return value.toString();
+            buffer.setLength(0);
+            if (isInteger) {
+                final int  integer = tile.getSample(x, y, b);
+                final double value = integer;
+                if (Double.doubleToRawLongBits(value) != Double.doubleToRawLongBits(lastValue)) {
+                    // The `format` method invoked here is not the same than in `double` case.
+                    lastValueAsText = cellFormat.format(integer, buffer, formatField).toString();
+                    lastValue = value;
+                }
+            } else {
+                final double value = tile.getSampleDouble(x, y, b);
+                if (Double.doubleToRawLongBits(value) != Double.doubleToRawLongBits(lastValue)) {
+                    lastValueAsText = cellFormat.format(value, buffer, formatField).toString();
+                    lastValue = value;
+                }
+            }
+            return lastValueAsText;
         }
         return OUT_OF_BOUNDS;
     }
 
     /**
+     * Formats a row index or column index.
+     */
+    final String formatHeaderValue(final int index) {
+        buffer.setLength(0);
+        return headerFormat.format(index, buffer, formatField).toString();
+    }
+
+    /**
      * Creates a new instance of the skin responsible for rendering this grid view.
      * From the perspective of this {@link Control}, the {@link Skin} is a black box.
      * It listens and responds to changes in state of this grid view. This method is
diff --git a/application/sis-javafx/src/main/java/org/apache/sis/gui/coverage/GridViewSkin.java b/application/sis-javafx/src/main/java/org/apache/sis/gui/coverage/GridViewSkin.java
index 7f88e60..8b81e0d 100644
--- a/application/sis-javafx/src/main/java/org/apache/sis/gui/coverage/GridViewSkin.java
+++ b/application/sis-javafx/src/main/java/org/apache/sis/gui/coverage/GridViewSkin.java
@@ -19,9 +19,12 @@ package org.apache.sis.gui.coverage;
 import java.awt.image.RenderedImage;
 import javafx.beans.value.ChangeListener;
 import javafx.beans.value.ObservableValue;
+import javafx.collections.ObservableList;
 import javafx.scene.Node;
+import javafx.scene.control.ScrollBar;
 import javafx.scene.control.skin.VirtualFlow;
 import javafx.scene.control.skin.VirtualContainerBase;
+import javafx.scene.shape.Rectangle;
 
 
 /**
@@ -56,32 +59,86 @@ final class GridViewSkin extends VirtualContainerBase<GridView, GridRow> {
         view.cellWidth  .addListener(this::cellWidthChanged);
         view.headerWidth.addListener(this::cellWidthChanged);
         /*
+         * Rectangles for filling the background of the cells in the header row and header column.
+         * Those rectangles will be resized when the GridView size changes or cells size changes.
+         */
+        final Rectangle topBackground  = new Rectangle();
+        final Rectangle leftBackground = new Rectangle();
+        topBackground.setHeight(view.cellHeight.get());
+        leftBackground.setY(topBackground.getHeight());
+        leftBackground.widthProperty().bind(view.headerWidth);
+        leftBackground.fillProperty() .bind(view.headerBackground);
+        topBackground .fillProperty() .bind(view.headerBackground);
+        /*
          * The list of children is initially empty. We need to
          * add the virtual flow, otherwise nothing will appear.
          */
-        getChildren().add(flow);
+        getChildren().addAll(topBackground, leftBackground, flow);
+        flow.widthProperty() .addListener(this::gridSizeChanged);
+        flow.heightProperty().addListener(this::gridSizeChanged);
+    }
+
+    /**
+     * Invoked when the width or height of {@link GridView} changed. This method recomputes the size of
+     * the rectangles used for painting backgrounds. We listen to changes in width and height together
+     * because a change of width may show or hide the horizontal scroll bar, which change the height
+     * (and conversely for the vertical scroll bar).
+     */
+    private void gridSizeChanged(ObservableValue<? extends Number> property, Number oldValue, Number newValue) {
+        final Flow flow = (Flow) getVirtualFlow();
+        final ObservableList<Node> children = getChildren();
+        Rectangle r;
+        r = (Rectangle) children.get(0); r.setWidth (flow.getVisibleWidth()  - r.getX());
+        r = (Rectangle) children.get(1); r.setHeight(flow.getVisibleHeight() - r.getY());
     }
 
     /**
-     * Invoked when the value of {@link GridView#cellHeight} property changed.
+     * Invoked when the value of {@link GridView#cellHeight} property changed. This method copies the new value
+     * into {@link VirtualFlow#fixedCellSizeProperty()} after bounds check, then adjusts the size and position
+     * of rectangles filling the header background.
      */
     private void cellHeightChanged(ObservableValue<? extends Number> property, Number oldValue, Number newValue) {
-        getVirtualFlow().setFixedCellSize(Math.max(GridView.MIN_CELL_SIZE, newValue.intValue()));
+        final Flow flow = (Flow) getVirtualFlow();
+        final ObservableList<Node> children = getChildren();
+        final double height = Math.max(GridView.MIN_CELL_SIZE, newValue.doubleValue());
+        flow.setFixedCellSize(height);
+        Rectangle r;
+        r = (Rectangle) children.get(0); r.setHeight(height);
+        r = (Rectangle) children.get(1); r.setHeight(flow.getVisibleHeight() - height); r.setY(height);
     }
 
     /**
-     * Invoked when the cell width or cell spacing changed. This method notifies all children of the new width.
+     * Invoked when the cell width or cell spacing changed.
+     * This method notifies all children about the new width.
      */
     private void cellWidthChanged(ObservableValue<? extends Number> property, Number oldValue, Number newValue) {
         final double width = getSkinnable().getContentWidth();
         for (final Node child : getChildren()) {
-            if (child instanceof GridRow) {             // The first instance if not a GridRow.
+            if (child instanceof GridRow) {             // The first instances are not a GridRow.
                 ((GridRow) child).setPrefWidth(width);
             }
         }
     }
 
     /**
+     * Invoked when the content may have changed. If {@code all} is {@code true}, then everything
+     * may have changed including the number of rows and columns. If {@code all} is {@code false}
+     * then the number of rows and columns is assumed the same.
+     *
+     * @see GridView#contentChanged(boolean)
+     */
+    final void contentChanged(final boolean all) {
+        if (all) {
+            updateItemCount();
+        }
+        /*
+         * Following call may be redundant with `updateItemCount()` except if the number of
+         * rows did not changed, in which case `updateItemCount()` may have sent no event.
+         */
+        ((Flow) getVirtualFlow()).changed(null, null, null);
+    }
+
+    /**
      * Creates the virtual flow used by this {@link GridViewSkin}. The virtual flow
      * created by this method registers a listener for horizontal scroll bar events.
      */
@@ -116,6 +173,30 @@ final class GridViewSkin extends VirtualContainerBase<GridView, GridRow> {
         }
 
         /**
+         * Returns the width of the view port area, not counting the vertical scroll bar.
+         */
+        final double getVisibleWidth() {
+            double width = getWidth();
+            final ScrollBar bar = getVbar();
+            if (bar.isVisible()) {
+                width -= bar.getWidth();
+            }
+            return width;
+        }
+
+        /**
+         * Returns the height of the view port area, not counting the horizontal scroll bar.
+         */
+        final double getVisibleHeight() {
+            double height = getHeight();
+            final ScrollBar bar = getHbar();
+            if (bar.isVisible()) {
+                height -= bar.getHeight();
+            }
+            return height;
+        }
+
+        /**
          * Invoked when the content to show changed because of a change in a property.
          * The most important event is a change in the position of horizontal scroll bar,
          * which is handled as a change of content because we will need to change values
@@ -160,8 +241,9 @@ final class GridViewSkin extends VirtualContainerBase<GridView, GridRow> {
     }
 
     /**
-     * Called during the layout pass of the scene graph. Current implementation sets the virtual
-     * flow size to the given size.
+     * Called during the layout pass of the scene graph. The (x,y) coordinates are usually zero
+     * and the (width, height) are the size of the control as shown (not the full content size).
+     * Current implementation sets the virtual flow size to the given size.
      */
     @Override
     protected void layoutChildren(final double x, final double y, final double width, final double height) {