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 ad004b9f2b63b9cc2a79060748bfa2aee0a7d5fc Author: Martin Desruisseaux <[email protected]> AuthorDate: Wed May 25 12:24:53 2022 +0200 Show the currently selected value in the status bar during slider adjustment. --- .../apache/sis/gui/coverage/GridSliceSelector.java | 163 ++++++++++++++------- .../apache/sis/gui/coverage/ViewAndControls.java | 7 +- .../java/org/apache/sis/gui/map/StatusBar.java | 35 ++++- .../java/org/apache/sis/internal/gui/Styles.java | 2 +- 4 files changed, 146 insertions(+), 61 deletions(-) diff --git a/application/sis-javafx/src/main/java/org/apache/sis/gui/coverage/GridSliceSelector.java b/application/sis-javafx/src/main/java/org/apache/sis/gui/coverage/GridSliceSelector.java index 0868d3f495..5410efc0ba 100644 --- a/application/sis-javafx/src/main/java/org/apache/sis/gui/coverage/GridSliceSelector.java +++ b/application/sis-javafx/src/main/java/org/apache/sis/gui/coverage/GridSliceSelector.java @@ -38,6 +38,7 @@ import javafx.beans.property.ObjectProperty; import javafx.beans.property.ReadOnlyProperty; import javafx.beans.property.ReadOnlyObjectWrapper; import javafx.beans.property.SimpleObjectProperty; +import javax.measure.Unit; import org.opengis.geometry.Envelope; import org.opengis.util.FactoryException; import org.opengis.metadata.spatial.DimensionNameType; @@ -55,6 +56,8 @@ import org.apache.sis.internal.system.Modules; import org.apache.sis.math.DecimalFunctions; import org.apache.sis.math.MathFunctions; import org.apache.sis.gui.Widget; +import org.apache.sis.gui.map.StatusBar; +import org.apache.sis.measure.UnitFormat; import org.apache.sis.referencing.CRS; import org.apache.sis.util.iso.Types; import org.apache.sis.util.logging.Logging; @@ -128,17 +131,32 @@ public class GridSliceSelector extends Widget { /** * The object to use for formatting dates without time, created when first needed. * - * @see #getDateFormat(boolean) + * @see #getDateFormat(boolean, boolean) */ private DateFormat dateFormat; /** * The object to use for formatting dates with time, created when first needed. * - * @see #getDateFormat(boolean) + * @see #getDateFormat(boolean, boolean) */ private DateFormat dateAndTimeFormat; + /** + * The object to use for formatting long date and time in the status bar, created when first needed. + * + * @see #getDateFormat(boolean, boolean) + */ + private DateFormat longDateFormat; + + /** + * The status bar where to report the selected position during adjustment, or {@code null} if none. + * + * @todo Should be replaced by a {@link ReadOnlyProperty} which computes the value only if there is + * at least one listener. + */ + StatusBar status; + /** * Creates a new widget. * @@ -205,8 +223,9 @@ public class GridSliceSelector extends Widget { GridPane.setValignment(label, VPos.TOP); converter = new Converter(); slider.setLabelFormatter(converter); - slider.widthProperty().addListener(converter); - slider.valueChangingProperty().addListener(new Listener(dim)); + slider.widthProperty().addListener((p,o,n) -> converter.setTickSpacing(slider, n.doubleValue())); + slider.valueProperty().addListener((p,o,n) -> converter.formatMessage(Math.rint(n.doubleValue()))); + slider.valueChangingProperty().addListener(converter); } /* * Configure the slider for the current grid axis. @@ -252,8 +271,16 @@ public class GridSliceSelector extends Widget { /** * Handle conversion of grid indices to "real world" coordinates or dates. + * This is also a listener notified when the position of a slider changed. + * We take the change only after the user finished to drag the slider + * in order to avoid causing to many load requests. */ - private final class Converter extends StringConverter<Double> implements ChangeListener<Number> { + private final class Converter extends StringConverter<Double> implements ChangeListener<Boolean> { + /** + * Index of the grid axis where the position changed. + */ + private int dimension; + /** * Conversion from grid indices to "real world" coordinates, or {@code null} if none. */ @@ -289,6 +316,11 @@ public class GridSliceSelector extends Widget { */ private double spacingNumerator; + /** + * The unit name to append after the value in verbose display, or {@code null} if none. + */ + private String unitName; + /** * Creates a new converter. */ @@ -309,8 +341,10 @@ public class GridSliceSelector extends Widget { final void configure(final GridGeometry gg, final TransformSeparator ts, final int dim, final double min, final double max, final Envelope env, final double[] res) { + dimension = dim; gridToCRS = null; axis = null; + unitName = null; timeCRS = null; resolution = 1; timeResolutionThreshold = Double.NaN; @@ -325,6 +359,14 @@ public class GridSliceSelector extends Widget { final CoordinateReferenceSystem c = CRS.getComponentAt(crs, targetDim, targetDim+1); timeCRS = (c instanceof TemporalCRS) ? DefaultTemporalCRS.castOrCopy((TemporalCRS) c) : null; axis = crs.getCoordinateSystem().getAxis(targetDim); + if (timeCRS == null) { + final Unit<?> unit = axis.getUnit(); + if (unit != null) { + final UnitFormat f = new UnitFormat(locale != null ? locale : Locale.getDefault()); + f.setStyle(UnitFormat.Style.NAME); + unitName = ' ' + f.format(unit); + } + } } /* * `span` and `resolution` must be updated together, assuming linear conversion. @@ -334,7 +376,9 @@ public class GridSliceSelector extends Widget { if (env != null && res != null && res[targetDim] > 0) { resolution = res[targetDim]; span = env.getSpan(targetDim); - timeResolutionThreshold = timeCRS.toValue(Duration.ofDays(1)); + if (timeCRS != null) { + timeResolutionThreshold = timeCRS.toValue(Duration.ofDays(1)); + } } } catch (FactoryException | ClassCastException e) { Logging.ignorableException(Logger.getLogger(Modules.APPLICATION), GridSliceSelector.class, @@ -372,20 +416,45 @@ public class GridSliceSelector extends Widget { } /** - * Invoked when the slider changed its size. - * This method updates the number of ticks based on available space. + * Invoked when the user begins of finished to adjust the slider position. + * The grid extent is updated only after the user finished to adjust. */ @Override - public void changed(final ObservableValue<? extends Number> property, final Number oldValue, final Number newValue) { - setTickSpacing((Slider) ((ReadOnlyProperty) property).getBean(), newValue.doubleValue()); + public void changed(final ObservableValue<? extends Boolean> property, final Boolean oldValue, final Boolean newValue) { + final Slider slider = (Slider) ((ReadOnlyProperty<?>) property).getBean(); + final long position = Math.round(slider.getValue()); + if (newValue) { + formatMessage(position); + } else { + final StatusBar bar = status; + if (bar != null) { + bar.setInfoMessage(null); // Clear the position that we wrote in the status bar. + } + final GridExtent extent = selectedExtent.get(); + if (extent != null && position != extent.getLow(dimension)) { + selectedExtent.set(extent.setRange(dimension, position, position)); + } + } + } + + /** + * Invoked when the slider changed its position. + * This method updates the message in the status bar. + */ + final void formatMessage(final double position) { + final StatusBar bar = status; + if (bar != null) { + bar.setInfoMessage(toString(position, true)); + } } /** * Converts a grid index to a string representation. + * + * @param value the grid index. Should be an integer value. + * @param verbose whether to use a verbose format. */ - @Override - public String toString(final Double index) { - double value = index; + private String toString(double value, final boolean verbose) { double derivative; int numDigits; if (gridToCRS != null) try { @@ -399,16 +468,29 @@ public class GridSliceSelector extends Widget { numDigits = 0; } if (timeCRS != null) { - final DateFormat f = getDateFormat(derivative < timeResolutionThreshold); + final DateFormat f = getDateFormat(verbose, derivative < timeResolutionThreshold); return f.format(timeCRS.toDate(value)); } else { final NumberFormat f = getNumberFormat(); f.setMinimumFractionDigits(numDigits); f.setMaximumFractionDigits(numDigits); - return f.format(value); + String text = f.format(value); + if (verbose && unitName != null) { + text = text.concat(unitName); + } + return text; } } + /** + * Converts a grid index to a string representation. + * The given value is rounded to nearest integer for making sure that it describes a cell position. + */ + @Override + public String toString(final Double index) { + return toString(Math.rint(index), false); + } + /** * Converts a string representation to a grid index. * This method is defined as a matter of principle but should not be invoked. @@ -418,7 +500,7 @@ public class GridSliceSelector extends Widget { double value; try { if (timeCRS != null) { - value = timeCRS.toValue(getDateFormat(true).parse(text)); + value = timeCRS.toValue(getDateFormat(false, true).parse(text)); } else { value = getNumberFormat().parse(text).doubleValue(); } @@ -447,10 +529,18 @@ public class GridSliceSelector extends Widget { /** * Returns the object to use for formatting dates. * + * @param verbose whether to use a verbose format for display on as a message (as opposed to label). * @param withTime {@code false} for dates only, or {@code true} for dates with times. */ - private DateFormat getDateFormat(final boolean withTime) { - if (withTime) { + private DateFormat getDateFormat(final boolean verbose, final boolean withTime) { + if (verbose) { + if (longDateFormat == null) { + longDateFormat = (locale != null) + ? DateFormat.getDateTimeInstance(DateFormat.LONG, DateFormat.LONG, locale) + : DateFormat.getDateTimeInstance(DateFormat.LONG, DateFormat.LONG); + } + return longDateFormat; + } else if (withTime) { if (dateAndTimeFormat == null) { dateAndTimeFormat = (locale != null) ? DateFormat.getDateTimeInstance(DateFormat.SHORT, DateFormat.SHORT, locale) @@ -470,43 +560,6 @@ public class GridSliceSelector extends Widget { - /** - * Listener notified when the position of a slider changed. We take the change only after - * the user finished to drag the slider in order to avoid causing to many load requests. - */ - private final class Listener implements ChangeListener<Boolean> { - /** - * Index of the grid axis where the position changed. - */ - private final int dimension; - - /** - * Creates a new listener for the grid axis on the specified dimension. - * - * @param dimension index of the grid axis where the position can change. - */ - Listener(final int dimension) { - this.dimension = dimension; - } - - /** - * Invoked when the slider changed its position. - */ - @Override - public void changed(final ObservableValue<? extends Boolean> property, final Boolean oldValue, final Boolean newValue) { - if (!newValue) { - final GridExtent extent = selectedExtent.get(); - if (extent != null) { - final Slider slider = (Slider) ((ReadOnlyProperty) property).getBean(); - final long p = Math.round(slider.getValue()); - if (p != extent.getLow(dimension)) { - selectedExtent.set(extent.setRange(dimension, p, p)); - } - } - } - } - } - /** * Returns the property for the currently selected grid extent. * This value varies after the user finished to drag the slider. diff --git a/application/sis-javafx/src/main/java/org/apache/sis/gui/coverage/ViewAndControls.java b/application/sis-javafx/src/main/java/org/apache/sis/gui/coverage/ViewAndControls.java index bbf6bebc18..3e34ad71d3 100644 --- a/application/sis-javafx/src/main/java/org/apache/sis/gui/coverage/ViewAndControls.java +++ b/application/sis-javafx/src/main/java/org/apache/sis/gui/coverage/ViewAndControls.java @@ -24,6 +24,7 @@ import javafx.scene.control.Toggle; import javafx.scene.control.TitledPane; import javafx.scene.control.Accordion; import javafx.scene.control.SplitPane; +import javafx.scene.control.Separator; import javafx.scene.layout.Priority; import javafx.scene.layout.Region; import javafx.scene.layout.VBox; @@ -70,7 +71,7 @@ abstract class ViewAndControls { /** * Index of {@link #sliceSelector} in the list of children of {@link #viewAndNavigation}. */ - private static final int SLICE_SELECTOR_INDEX = 2; + private static final int SLICE_SELECTOR_INDEX = 3; /** * The toolbar button for selecting this view. @@ -133,8 +134,10 @@ abstract class ViewAndControls { VBox.setVgrow(view, Priority.ALWAYS); VBox.setVgrow(bar, Priority.NEVER); VBox.setVgrow(nav, Priority.NEVER); - viewAndNavigation.getChildren().setAll(view, bar); // `nav` will be added only when non-empty. + final Separator sep = new Separator(); + viewAndNavigation.getChildren().setAll(view, sep, bar); // `nav` will be added only when non-empty. SplitPane.setResizableWithParent(viewAndNavigation, Boolean.TRUE); + sliceSelector.status = status; } /** diff --git a/application/sis-javafx/src/main/java/org/apache/sis/gui/map/StatusBar.java b/application/sis-javafx/src/main/java/org/apache/sis/gui/map/StatusBar.java index 6688f8d3b2..672b10ba7c 100644 --- a/application/sis-javafx/src/main/java/org/apache/sis/gui/map/StatusBar.java +++ b/application/sis-javafx/src/main/java/org/apache/sis/gui/map/StatusBar.java @@ -413,7 +413,6 @@ public class StatusBar extends Widget implements EventHandler<MouseEvent> { message = new Label(); message.setVisible(false); // Waiting for getting a message to display. - message.setTextFill(Styles.ERROR_TEXT); message.setMaxWidth(Double.POSITIVE_INFINITY); HBox.setHgrow(message, Priority.ALWAYS); @@ -1056,7 +1055,7 @@ public class StatusBar extends Widget implements EventHandler<MouseEvent> { } /* * Make sure that there is enough space for keeping the coordinates always visible. - * This is the needed if there is an error message on the left which may be long. + * This is needed if there is an error message on the left which may be long. */ if (text.length() > maximalPositionLength) { maximalPositionLength = text.length(); @@ -1249,17 +1248,47 @@ public class StatusBar extends Widget implements EventHandler<MouseEvent> { return true; } + /** + * Returns the message currently shown. It may be an error message or an informative message. + * + * @return the current message, or an empty value if none. + * + * @since 1.3 + */ + public Optional<String> getMessage() { + return Optional.ofNullable(message.getText()); + } + /** * Returns the error message currently shown. * * @return the current error message, or an empty value if none. + * + * @deprecated Renamed {@link #getMessage()}. */ + @Deprecated public Optional<String> getErrorMessage() { return Optional.ofNullable(message.getText()); } /** - * Show or hide an error message on the status bar, optionally with a button showing details in a dialog box. + * Shows or hides an informative message on the status bar. + * The message should be temporary, for example for telling that a loading is in progress. + * + * @param text the message to show, or {@code null} if none. + * + * @since 1.3 + */ + public void setInfoMessage(String text) { + text = Strings.trimOrNull(text); + message.setVisible(text != null); + message.setGraphic(null); + message.setText(text); + message.setTextFill(Styles.LOADING_TEXT); + } + + /** + * Shows or hides an error message on the status bar, optionally with a button showing details in a dialog box. * The {@code text} argument specifies the message to show on the status bar. * If {@code text} is null, the message will be taken from the {@code details} if non-null. * If {@code details} is also null, then the error message will be hidden. 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 8e1422bb06..bb260cc154 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 @@ -84,7 +84,7 @@ public final class Styles extends Static { /** * Color of the text saying that data are in process of being loaded. */ - public static final Color LOADING_TEXT = Color.BLUE; + public static final Color LOADING_TEXT = Color.STEELBLUE; /** * Color of text for authority codes.
