sis-commits mailing list archives

Site index · List index
Message view « Date » · « Thread »
Top « Date » · « Thread »
From desruisse...@apache.org
Subject [sis] 03/04: Provide control on the number of fraction digits shown in the GridView cells.
Date Tue, 18 Feb 2020 20:03:45 GMT
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 cbcfca691628345578e3b826c1785c753b9ca3ef
Author: Martin Desruisseaux <martin.desruisseaux@geomatys.com>
AuthorDate: Tue Feb 18 12:21:02 2020 +0100

    Provide control on the number of fraction digits shown in the GridView cells.
---
 .../org/apache/sis/gui/coverage/CellFormat.java    | 134 ++++++++++++++++++++-
 .../apache/sis/gui/coverage/CoverageExplorer.java  |  44 ++++---
 .../java/org/apache/sis/gui/coverage/GridView.java |  24 +++-
 .../org/apache/sis/internal/gui/RecentChoices.java |  28 +++++
 .../java/org/apache/sis/internal/gui/Styles.java   |  27 +++++
 5 files changed, 236 insertions(+), 21 deletions(-)

diff --git a/application/sis-javafx/src/main/java/org/apache/sis/gui/coverage/CellFormat.java
b/application/sis-javafx/src/main/java/org/apache/sis/gui/coverage/CellFormat.java
index 911cf29..f953fdb 100644
--- a/application/sis-javafx/src/main/java/org/apache/sis/gui/coverage/CellFormat.java
+++ b/application/sis-javafx/src/main/java/org/apache/sis/gui/coverage/CellFormat.java
@@ -22,21 +22,29 @@ import java.text.NumberFormat;
 import java.text.FieldPosition;
 import java.awt.image.Raster;
 import java.awt.image.RenderedImage;
+import javafx.beans.property.SimpleStringProperty;
+import javafx.scene.control.ComboBox;
+import javafx.scene.control.Tooltip;
+import javafx.scene.layout.Background;
 import org.apache.sis.image.PlanarImage;
 import org.apache.sis.math.DecimalFunctions;
 import org.apache.sis.util.Numbers;
 import org.apache.sis.util.Workaround;
+import org.apache.sis.internal.gui.Styles;
+import org.apache.sis.internal.gui.RecentChoices;
 
 
 /**
  * Formatter for cell values with a number of fraction digits determined from the sample
value resolution.
+ * The property value is the localized format pattern as produced by {@link DecimalFormat#toLocalizedPattern()}.
+ * This property is usually available but not always; see {@link #hasPattern()}.
  *
  * @author  Martin Desruisseaux (Geomatys)
  * @version 1.1
  * @since   1.1
  * @module
  */
-final class CellFormat {
+final class CellFormat extends SimpleStringProperty {
     /**
      * The "classic" number format pattern (as opposed to scientific notation). This is non-null
only after
      * {@link #cellFormat} switched to scientific notation and is used for switching back
to classic notation.
@@ -85,12 +93,128 @@ final class CellFormat {
     boolean dataTypeisInteger;
 
     /**
-     * Creates a new cell formatter.
+     * Temporarily set to {@code true} when the user selects or enters a new pattern in a
GUI control, then
+     * reset to {@code false} after the new values has been set. This is a safety against
recursive calls
+     * to {@link #patternSelected(ComboBox, String)} because of bi-directional change listeners.
      */
-    CellFormat() {
+    private boolean isAdjusting;
+
+    /**
+     * Creates a new cell formatter which is also a {@code "cellFormatPattern"} property
for the given bean.
+     */
+    CellFormat(final GridView bean) {
+        super(bean, "cellFormatPattern");
         cellFormat  = NumberFormat.getInstance();
         formatField = new FieldPosition(0);
         buffer      = new StringBuffer();
+        updatePropertyValue();
+    }
+
+    /**
+     * Returns whether the format can be configured with a pattern. If this method returns
{@code false},
+     * then the {@link GridView#cellFormatPattern()} property is not available.
+     */
+    final boolean hasPattern() {
+        return (cellFormat instanceof DecimalFormat);
+    }
+
+    /**
+     * Sets this property to the current {@link DecimalFormat} pattern and notifies all listeners
+     * if the new pattern is different than the old one. This method needs to be invoked
explicitly
+     * after the {@link #cellFormat} has been configured.
+     */
+    private void updatePropertyValue() {
+        if (cellFormat instanceof DecimalFormat) {
+            super.setValue(((DecimalFormat) cellFormat).toLocalizedPattern());
+        }
+    }
+
+    /**
+     * Invoked when a new pattern is set programmatically on the {@link GridView#cellFormatPattern()}
property.
+     * This is also invoked when the used selected or entered a new pattern, by user action
through the GUI.
+     *
+     * @param  pattern  the new pattern.
+     * @throws NullPointerException if {@code pattern} is {@code null}.
+     * @throws IllegalArgumentException if the given pattern is invalid.
+     */
+    @Override
+    public void setValue(final String pattern) {
+        if (cellFormat instanceof DecimalFormat) {
+            ((DecimalFormat) cellFormat).applyLocalizedPattern(pattern);
+            updatePropertyValue();
+            ((GridView) getBean()).contentChanged(false);
+        }
+    }
+
+    /**
+     * Invoked when the user selects or enters a new pattern. This method should not be invoked
explicitly.
+     * This is a callback to be invoked by the {@link javafx.scene.control.SingleSelectionModel}
of a control.
+     *
+     * @param  choices   the control where format pattern is selected.
+     * @param  newValue  the new format pattern.
+     */
+    private void patternSelected(final ComboBox<String> choices, final String newValue)
{
+        if (!isAdjusting) {
+            Background background;
+            String message;
+            try {
+                isAdjusting = true;
+                setValue(newValue);
+                background = null;
+                message = null;
+            } catch (IllegalArgumentException e) {
+                background = Styles.ERROR_BACKGROUND;
+                message = e.getLocalizedMessage();
+            } finally {
+                isAdjusting = false;
+            }
+            Tooltip tooltip = null;
+            if (message != null) {
+                tooltip = choices.getTooltip();
+                if (tooltip != null) {
+                    tooltip.setText(message);
+                } else {
+                    tooltip = new Tooltip(message);
+                }
+            }
+            choices.setTooltip(tooltip);
+            choices.getEditor().setBackground(background);
+        }
+    }
+
+    /**
+     * An editable combo box which remember the most recently used values,
+     * or {@code null} if the {@link NumberFormat} does not support patterns.
+     */
+    final ComboBox<String> createEditor() {
+        if (!hasPattern()) {
+            return null;
+        }
+        /*
+         * Create a few pre-defined choices of patterns with various number of fraction digits.
+         */
+        final int min = cellFormat.getMinimumFractionDigits();
+        final int max = cellFormat.getMaximumFractionDigits();
+        final String[] patterns = new String[max + 2];
+        patterns[max + 1] = getValue();
+        cellFormat.setMinimumFractionDigits(max);
+        for (int n=max; n >= 0; n--) {
+            cellFormat.setMaximumFractionDigits(n);
+            patterns[n] = ((DecimalFormat) cellFormat).toLocalizedPattern();
+        }
+        cellFormat.setMinimumFractionDigits(min);           // Restore previous setting.
+        cellFormat.setMaximumFractionDigits(max);
+        /*
+         * Create the combo-box with above patterns and register listeners in both directions.
+         */
+        final ComboBox<String> choices = new ComboBox<>();
+        choices.setEditable(true);
+        choices.getItems().setAll(patterns);
+        choices.getSelectionModel().selectFirst();
+        choices.getSelectionModel().selectedItemProperty().addListener((e,o,n) -> patternSelected(choices,
n));
+        addListener((e,o,n) -> RecentChoices.setInList(choices, n));
+        choices.setMaxWidth(Double.POSITIVE_INFINITY);
+        return choices;
     }
 
     /**
@@ -134,6 +258,7 @@ final class CellFormat {
         }
         buffer.setLength(0);
         formatCell(lastValue);
+        updatePropertyValue();
     }
 
     /**
@@ -177,7 +302,8 @@ final class CellFormat {
     }
 
     /**
-     * Formats the given sample value.
+     * Formats the given sample value. This is used for formatting the values from another
source
+     * than a {@link Raster}, such as a {@link org.apache.sis.coverage.SampleDimension}.
      */
     final String format(final Number value) {
         buffer.setLength(0);
diff --git a/application/sis-javafx/src/main/java/org/apache/sis/gui/coverage/CoverageExplorer.java
b/application/sis-javafx/src/main/java/org/apache/sis/gui/coverage/CoverageExplorer.java
index 89d98aa..b31d6a2 100644
--- a/application/sis-javafx/src/main/java/org/apache/sis/gui/coverage/CoverageExplorer.java
+++ b/application/sis-javafx/src/main/java/org/apache/sis/gui/coverage/CoverageExplorer.java
@@ -25,6 +25,7 @@ import javafx.beans.value.ObservableValue;
 import javafx.collections.ObservableList;
 import javafx.geometry.Insets;
 import javafx.scene.control.Accordion;
+import javafx.scene.control.Control;
 import javafx.scene.control.Label;
 import javafx.scene.control.Slider;
 import javafx.scene.control.SplitPane;
@@ -136,22 +137,24 @@ public class CoverageExplorer {
             gp.setBorder(GROUP_BORDER);
             gp.setVgap(9);
             gp.setHgap(9);
-            int row = 0;
-            do {
-                final DoubleProperty property;
-                final double min, max;
+addRows:    for (int row = 0;; row++) {
+                final Control control;
                 final short key;
-                if (row == 0) {key = Vocabulary.Keys.Width;   property = gridView.cellWidth;
  min = 30; max = 200;}
-                else          {key = Vocabulary.Keys.Height;  property = gridView.cellHeight;
 min = 10; max =  50;}
-                final Label  label  = new Label(vocabulary.getLabel(key));
-                final Slider slider = new Slider(min, max, property.getValue());
-                property.bind(slider.valueProperty());
-                slider.setShowTickMarks(false);
-                label.setLabelFor(slider);
-                GridPane.setConstraints(label,  0, row);
-                GridPane.setConstraints(slider, 1, row);
-                gp.getChildren().addAll(label, slider);
-            } while (++row <= 1);
+                switch (row) {
+                    case 0: key = Vocabulary.Keys.Width;  control = createSlider(gridView.cellWidth,
 30, 200); break;
+                    case 1: key = Vocabulary.Keys.Height; control = createSlider(gridView.cellHeight,
10, 50);  break;
+                    case 2: key = Vocabulary.Keys.Format; control = gridView.cellFormat.createEditor();
        break;
+                    default: break addRows;
+                }
+                if (control != null) {
+                    final Label label = new Label(vocabulary.getLabel(key));
+                    label.setLabelFor(control);
+                    GridPane.setConstraints(label,   0, row);
+                    GridPane.setConstraints(control, 1, row);
+                    gp.getChildren().addAll(label, control);
+                }
+            }
+            Styles.allRowSameHeight(gp);
             final Label label = new Label(vocabulary.getLabel(Vocabulary.Keys.Cells));
             label.setPadding(CAPTION_MARGIN);
             label.setLabelFor(gp);
@@ -177,6 +180,17 @@ public class CoverageExplorer {
     }
 
     /**
+     * Creates a new slider for the given range of values and bound to the specified properties.
+     * This is used for creating the sliders to shown in the "Display" pane.
+     */
+    private static Slider createSlider(final DoubleProperty property, final double min, final
double max) {
+        final Slider slider = new Slider(min, max, property.getValue());
+        property.bind(slider.valueProperty());
+        slider.setShowTickMarks(false);
+        return slider;
+    }
+
+    /**
      * Returns the region containing the grid view, band selector and any other control managed
      * by this {@code CoverageExplorer}. The subclass is implementation dependent and may
change
      * in any future version.
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 f343ad7..b4b171d 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
@@ -16,6 +16,7 @@
  */
 package org.apache.sis.gui.coverage;
 
+import java.util.Optional;
 import java.text.NumberFormat;
 import java.awt.Rectangle;
 import java.awt.image.Raster;
@@ -29,6 +30,7 @@ import javafx.beans.property.ObjectProperty;
 import javafx.beans.property.SimpleDoubleProperty;
 import javafx.beans.property.SimpleIntegerProperty;
 import javafx.beans.property.SimpleObjectProperty;
+import javafx.beans.property.StringProperty;
 import javafx.beans.value.ObservableValue;
 import javafx.concurrent.WorkerStateEvent;
 import javafx.scene.control.Control;
@@ -184,7 +186,12 @@ public class GridView extends Control {
     private final NumberFormat headerFormat;
 
     /**
-     * The formatter to use for writing sample values.
+     * The formatter to use for writing sample values. This is also the the property for
the localized format pattern.
+     * Note that this pattern depends on current locale. It is provided for user interactions
(i.e. in a GUI control)
+     * instead than programmatic action.
+     *
+     * @see #cellFormatPattern()
+     * @see java.text.DecimalFormat#toLocalizedPattern()
      */
     final CellFormat cellFormat;
 
@@ -201,7 +208,7 @@ public class GridView extends Control {
         cellSpacing      = new SimpleDoubleProperty  (this, "cellSpacing",  4);
         headerBackground = new SimpleObjectProperty<>(this, "headerBackground", Color.GAINSBORO);
         headerFormat     = NumberFormat.getIntegerInstance();
-        cellFormat       = new CellFormat();
+        cellFormat       = new CellFormat(this);
         tileWidth        = 1;
         tileHeight       = 1;       // For avoiding division by zero.
 
@@ -497,6 +504,19 @@ public class GridView extends Control {
     }
 
     /**
+     * The property for the pattern of values in cells. Note that this pattern depends on
current locale.
+     * It is provided for user interactions (i.e. in a GUI control) instead than programmatic
action.
+     *
+     * @return the <em>localized</em> format pattern property, or an empty value
if the {@link NumberFormat}
+     *         used for writing cell values is not an instance of {@link java.text.DecimalFormat}.
+     *
+     * @see java.text.DecimalFormat#toLocalizedPattern()
+     */
+    public final Optional<StringProperty> cellFormatPattern() {
+        return cellFormat.hasPattern() ? Optional.of(cellFormat) : Optional.empty();
+    }
+
+    /**
      * Converts cell indices to pixel indices. They are often the same indices, but may differ
if the
      * {@link RenderedImage} uses a coordinate system where the coordinates of the upper-left
corner
      * is not (0,0).
diff --git a/application/sis-javafx/src/main/java/org/apache/sis/internal/gui/RecentChoices.java
b/application/sis-javafx/src/main/java/org/apache/sis/internal/gui/RecentChoices.java
index 1da17a7..eb0be43 100644
--- a/application/sis-javafx/src/main/java/org/apache/sis/internal/gui/RecentChoices.java
+++ b/application/sis-javafx/src/main/java/org/apache/sis/internal/gui/RecentChoices.java
@@ -19,6 +19,8 @@ package org.apache.sis.internal.gui;
 import java.io.File;
 import java.util.List;
 import java.util.prefs.Preferences;
+import javafx.scene.control.ComboBox;
+import javafx.collections.ObservableList;
 
 
 /**
@@ -92,4 +94,30 @@ public final class RecentChoices {
         }
         return parent;
     }
+
+    /**
+     * Sets a value in a list of choices provided by an editable combo box.
+     * The combo box will remember the last choices, up to an arbitrary limit.
+     * The most recently used choices appear firsT.
+     *
+     * @param  <T>       type of values in the combo box.
+     * @param  choices   the list of choices to update.
+     * @param  newValue  the new choice.
+     */
+    public static <T> void setInList(final ComboBox<T> choices, final T newValue)
{
+        final ObservableList<T> items = choices.getItems();
+        final int p = items.indexOf(newValue);
+        if (p != 0) {
+            if (p > 0) {
+                items.remove(p);
+            } else {
+                final int count = items.size();
+                if (count >= 20) {
+                    items.remove(count - 1);
+                }
+            }
+            items.add(0, newValue);
+        }
+        choices.getSelectionModel().selectFirst();
+    }
 }
diff --git a/application/sis-javafx/src/main/java/org/apache/sis/internal/gui/Styles.java
b/application/sis-javafx/src/main/java/org/apache/sis/internal/gui/Styles.java
index a6eb094..4e092ad 100644
--- a/application/sis-javafx/src/main/java/org/apache/sis/internal/gui/Styles.java
+++ b/application/sis-javafx/src/main/java/org/apache/sis/internal/gui/Styles.java
@@ -16,10 +16,15 @@
  */
 package org.apache.sis.internal.gui;
 
+import java.util.Arrays;
 import java.io.IOException;
 import java.io.InputStream;
 import javafx.scene.paint.Color;
 import javafx.scene.image.Image;
+import javafx.scene.layout.Background;
+import javafx.scene.layout.BackgroundFill;
+import javafx.scene.layout.GridPane;
+import javafx.scene.layout.RowConstraints;
 import org.apache.sis.util.logging.Logging;
 import org.apache.sis.internal.system.Modules;
 
@@ -29,6 +34,9 @@ import org.apache.sis.internal.system.Modules;
  * This provides a single place to revisit if we learn more about how
  * to make those color more dynamic with JavaFX styling.
  *
+ * <p>This class also opportunistically provides a few utility methods
+ * related to appearance.</p>
+ *
  * @author  Martin Desruisseaux (Geomatys)
  * @version 1.1
  * @since   1.1
@@ -87,6 +95,12 @@ public final class Styles {
     public static final Color SELECTION_BACKGROUND = Color.LIGHTBLUE;
 
     /**
+     * The background for cell having an illegal input value.
+     */
+    public static final Background ERROR_BACKGROUND =
+            new Background(new BackgroundFill(Color.LIGHTPINK, null, null));
+
+    /**
      * Do not allow instantiation of this class.
      */
     private Styles() {
@@ -116,4 +130,17 @@ public final class Styles {
         }
         return image;
     }
+
+    /**
+     * Sets all rows in the given grid pane to the same height.
+     *
+     * @param  gp  the grid pane in which to set row constraints.
+     */
+    public static void allRowSameHeight(final GridPane gp) {
+        final RowConstraints[] constraints = new RowConstraints[gp.getRowCount()];
+        final RowConstraints c = new RowConstraints();
+        c.setPercentHeight(100.0 / constraints.length);
+        Arrays.fill(constraints, c);
+        gp.getRowConstraints().setAll(constraints);
+    }
 }


Mime
View raw message