sis-commits mailing list archives

Site index · List index
Message view « Date » · « Thread »
Top « Date » · « Thread »
From desruisse...@apache.org
Subject [sis] 01/03: Improve the behavior when entering new isoline values: - Sort by increasing values. - Interpolate colors of new values between colors of existing values. - Transition to edition mode when a digit is typed (avoid the need to click on the cell first).
Date Mon, 11 Jan 2021 17:50:09 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 c034b46d2c8273abac232b99461ef5186e58116f
Author: Martin Desruisseaux <martin.desruisseaux@geomatys.com>
AuthorDate: Mon Jan 11 14:40:21 2021 +0100

    Improve the behavior when entering new isoline values:
    - Sort by increasing values.
    - Interpolate colors of new values between colors of existing values.
    - Transition to edition mode when a digit is typed (avoid the need to click on the cell
first).
---
 .../org/apache/sis/gui/coverage/IsolineTable.java  |  90 ++++++++++++--
 .../org/apache/sis/internal/gui/PropertyView.java  |   2 +-
 .../sis/internal/gui/control/FormatTableCell.java  | 130 ++++++++++++++++++++-
 3 files changed, 207 insertions(+), 15 deletions(-)

diff --git a/application/sis-javafx/src/main/java/org/apache/sis/gui/coverage/IsolineTable.java
b/application/sis-javafx/src/main/java/org/apache/sis/gui/coverage/IsolineTable.java
index 572e8ae..a7a87ec 100644
--- a/application/sis-javafx/src/main/java/org/apache/sis/gui/coverage/IsolineTable.java
+++ b/application/sis-javafx/src/main/java/org/apache/sis/gui/coverage/IsolineTable.java
@@ -92,20 +92,85 @@ final class IsolineTable extends ColorColumnHandler<IsolineLevel>
{
      */
     private static void commitEdit(final TableColumn.CellEditEvent<IsolineLevel,Number>
event) {
         final IsolineLevel level = event.getRowValue();
-        final Number value = event.getNewValue();
-        level.value.set(value != null ? value.doubleValue() : Double.NaN);
-        if (level.color.get() == null) {
-            level.color.set(new ColorRamp(Color.BLACK));
-        }
+        final Number obj = event.getNewValue();
+        final double value = (obj != null) ? obj.doubleValue() : Double.NaN;
+        level.value.set(value);
         level.visible.set(true);
         /*
-         * If the edited line was the insertion row, we need to add a new insertion row.
+         * Search for index where to move the row in order to keep ascending value order.
+         * The algorithm below is okay if the new position is close to current position.
+         * A binary search would be more efficient in the general case, but it may not be
+         * worth the additional complexity. We do not use `items.sort(…)` because we want
+         * to move only one row and its new position will determine the default color.
          */
-        final ObservableList<IsolineLevel> items = event.getTableView().getItems();
+        final TableView<IsolineLevel> table = event.getTableView();
+        final ObservableList<IsolineLevel> items = table.getItems();
         final int row = event.getTablePosition().getRow();
-        if (row >= items.size() - 1) {
+        int dst = row;
+        while (--dst >= 0) {
+            // Use `!` for stopping if `value` is NaN.
+            if (!(items.get(dst).value.get() >= value)) break;
+        }
+        final int size = items.size() - 1;                  // Excluding insertion row.
+        while (++dst < size) {
+            // No `!` for continuing until the end if `value` is NaN.
+            if (dst != row && items.get(dst).value.get() >= value) break;
+        }
+        if (dst != row) {
+            if (dst >= row) dst--;
+            items.add(dst, items.remove(row));
+            table.getSelectionModel().select(dst);
+        }
+        if (row >= size) {
+            // If the edited line was the insertion row, add a new insertion row.
             items.add(new IsolineLevel());
         }
+        /*
+         * If the row has no color (should be the case only for insertion row), interpolate
a default color
+         * using the isolines before and after the new row. If we are at the beginning or
end of the list,
+         * then interpolation will actually be an extrapolation.
+         */
+        if (level.color.get() == null) {
+            Color color = Color.BLACK;
+            final int last = items.size() - 2;      // -1 for excluding insertion row, -1
again for last item.
+            if (last >= 2) {                        // Need 3 items: the new row + 2 items
for interpolation.
+                int ilo = dst - 1;
+                int iup = dst + 1;
+                if (ilo < 0) {                       // (row index) == 0
+                    ilo = 1;
+                    iup = 2;
+                } else if (iup > last) {             // (row index) == last
+                    iup = last - 1;
+                    ilo = last - 2;
+                }
+                final IsolineLevel lo = items.get(ilo);
+                final IsolineLevel up = items.get(iup);
+                final double base = lo.value.get();
+                final double f = (value - base) / (up.value.get() - base);
+                final Color clo = lo.color.get().color();
+                final Color cup = up.color.get().color();
+                color = new Color(interpolate(f, clo.getRed(),     cup.getRed()),
+                                  interpolate(f, clo.getGreen(),   cup.getGreen()),
+                                  interpolate(f, clo.getBlue(),    cup.getBlue()),
+                                  interpolate(f, clo.getOpacity(), cup.getOpacity()));
+            }
+            level.color.set(new ColorRamp(color));
+        }
+    }
+
+    /**
+     * Interpolates or extrapolates a color component. Note: JavaFX provides a
+     * {@code Color.interpolate(…)} method, but it does not perform extrapolations.
+     *
+     * @param  f   factor between 0 and 1 for interpolations, outside that range for extrapolations.
+     * @param  lo  color component at f = 0.
+     * @param  up  color component at f = 1.
+     * @return interpolated or extrapolated color component.
+     *
+     * @see Color#interpolate(Color, double)
+     */
+    private static double interpolate(final double f, final double lo, final double up) {
+        return Math.max(0, Math.min(1, (up - lo) * f + lo));
     }
 
     /**
@@ -118,6 +183,7 @@ final class IsolineTable extends ColorColumnHandler<IsolineLevel>
{
     final TableView<IsolineLevel> createIsolineTable(final Vocabulary vocabulary) {
         /*
          * First column containing a checkbox for choosing whether the isoline should be
drawn or not.
+         * Header text is 🖉 (lower left pencil).
          */
         final TableColumn<IsolineLevel,Boolean> visible = new TableColumn<>("\uD83D\uDD89");
         visible.setCellFactory(CheckBoxTableCell.forTableColumn(visible));
@@ -131,8 +197,9 @@ final class IsolineTable extends ColorColumnHandler<IsolineLevel>
{
          * The number can be edited using a `NumberFormat` in current locale.
          */
         final TableColumn<IsolineLevel,Number> level = new TableColumn<>(vocabulary.getString(Vocabulary.Keys.Level));
+        final FormatTableCell.Trigger<IsolineLevel> trigger = new FormatTableCell.Trigger<>(level,
format);
+        level.setCellFactory((column) -> new FormatTableCell<>(Number.class, format,
trigger));
         level.setCellValueFactory((cell) -> cell.getValue().value);
-        level.setCellFactory((column) -> new FormatTableCell<>(Number.class, format));
         level.setOnEditCommit(IsolineTable::commitEdit);
         level.setSortable(false);                           // We will do our own sorting.
         level.setId("level");
@@ -144,9 +211,12 @@ final class IsolineTable extends ColorColumnHandler<IsolineLevel>
{
         table.getColumns().setAll(visible, level);
         addColumnTo(table, vocabulary);
         /*
-         * Add an empty row that user can edit for adding new data.
+         * Add an empty row that user can edit for adding new data. This row will automatically
enter in edition state
+         * when a digit is typed (this is the purpose of `trigger`). For making easier to
edit the cell in current row,
+         * a listener on F2 key (same as Excel and OpenOffice) is also registered.
          */
         table.getItems().add(new IsolineLevel());
+        trigger.registerTo(table);
         return table;
     }
 }
diff --git a/application/sis-javafx/src/main/java/org/apache/sis/internal/gui/PropertyView.java
b/application/sis-javafx/src/main/java/org/apache/sis/internal/gui/PropertyView.java
index 2a2a19f..36f854f 100644
--- a/application/sis-javafx/src/main/java/org/apache/sis/internal/gui/PropertyView.java
+++ b/application/sis-javafx/src/main/java/org/apache/sis/internal/gui/PropertyView.java
@@ -59,7 +59,7 @@ import org.apache.sis.util.resources.Vocabulary;
  * @since   1.1
  * @module
  */
-@SuppressWarnings("serial")     // Not intended to be serialized.
+@SuppressWarnings({"serial","CloneableImplementsClone"})            // Not intended to be
serialized.
 public final class PropertyView extends CompoundFormat<Object> {
     /**
      * The current property value. This is used for detecting changes.
diff --git a/application/sis-javafx/src/main/java/org/apache/sis/internal/gui/control/FormatTableCell.java
b/application/sis-javafx/src/main/java/org/apache/sis/internal/gui/control/FormatTableCell.java
index 8d07988..c4761ec 100644
--- a/application/sis-javafx/src/main/java/org/apache/sis/internal/gui/control/FormatTableCell.java
+++ b/application/sis-javafx/src/main/java/org/apache/sis/internal/gui/control/FormatTableCell.java
@@ -18,9 +18,16 @@ package org.apache.sis.internal.gui.control;
 
 import java.text.Format;
 import java.text.ParsePosition;
+import java.text.DecimalFormat;
+import java.text.DecimalFormatSymbols;
 import javafx.geometry.Pos;
+import javafx.event.EventHandler;
 import javafx.scene.input.KeyCode;
+import javafx.scene.input.KeyEvent;
 import javafx.scene.control.TableCell;
+import javafx.scene.control.TableColumn;
+import javafx.scene.control.TablePosition;
+import javafx.scene.control.TableView;
 import javafx.scene.control.TextField;
 import javafx.scene.layout.Background;
 import org.apache.sis.internal.gui.Styles;
@@ -53,6 +60,7 @@ public final class FormatTableCell<S,T> extends TableCell<S,T>
{
 
     /**
      * The format to use for parsing and formatting {@code <T>} values.
+     * The same instance can be shared by all cells in a table.
      */
     private final Format format;
 
@@ -68,14 +76,23 @@ public final class FormatTableCell<S,T> extends TableCell<S,T>
{
     private Background backgroundToRestore;
 
     /**
+     * A listener for enabling automatic transition to insertion state when a digit is pressed,
+     * or {@code null} if none. The same instance is shared by all cells in the same column.
+     */
+    private final Trigger<S> trigger;
+
+    /**
      * Creates a new table cell for parsing and formatting values of the given type.
      *
      * @param valueType  the type of objects expected and returned by {@code format}.
      * @param format     the format to use for parsing and formatting {@code <T>} values.
+     * @param trigger    listener for automatic transition to insertion state when a digit
is pressed,
+     *                   or {@code null} if none.
      */
-    public FormatTableCell(final Class<T> valueType, final Format format) {
+    public FormatTableCell(final Class<T> valueType, final Format format, final Trigger<S>
trigger) {
         this.valueType = valueType;
         this.format    = format;
+        this.trigger   = trigger;
         setAlignment(Pos.CENTER_LEFT);
     }
 
@@ -167,6 +184,8 @@ public final class FormatTableCell<S,T> extends TableCell<S,T>
{
     @Override
     public void startEdit() {
         super.startEdit();
+        String text = (trigger != null) ? trigger.initialText : null;
+        if (text == null) text = getText();
         if (editor != null) {
             /*
              * If the editor background color has been changed because of an error,
@@ -176,9 +195,9 @@ public final class FormatTableCell<S,T> extends TableCell<S,T>
{
                 editor.setBackground(backgroundToRestore);
                 backgroundToRestore = null;
             }
-            editor.setText(getText());
+            editor.setText(text);
         } else {
-            editor = new TextField(getText());
+            editor = new TextField(text);
             editor.setOnAction((event) -> {
                 event.consume();
                 parseAndCommit();
@@ -192,12 +211,18 @@ public final class FormatTableCell<S,T> extends TableCell<S,T>
{
         }
         setText(null);
         setGraphic(editor);
-        editor.selectAll();
         editor.requestFocus();
+        if (trigger.initialText == null) {
+            editor.selectAll();
+        } else {
+            editor.deselect();
+            editor.end();
+        }
     }
 
     /**
      * Invoked when edition has been cancelled.
+     * The current item value is reformatted.
      */
     @Override
     public void cancelEdit() {
@@ -205,4 +230,101 @@ public final class FormatTableCell<S,T> extends TableCell<S,T>
{
         setGraphic(null);
         setText(format(getItem()));
     }
+
+    /**
+     * A key event handler that can be registered on the table for transitioning automatically
to edition state
+     * in the insertion row when a digit is pressed. This trigger frees user from the need
to select the cell
+     * before editing the value. Current implementation reacts to digit keys, which is okay
for number format.
+     * Future version may be extended to more keys if there is a need for that.
+     *
+     * <p>Note: for making easier to edit current row instead than insertion row, it
is recommended to register
+     * also a listener for the F2 key (same key than Excel and OpenOffice). The {@link #registerTo(TableView)}
+     * convenience method does that.</p>
+     *
+     * @param  <S>  the type of elements contained in {@link javafx.scene.control.TableView}.
+     */
+    public static final class Trigger<S> implements EventHandler<KeyEvent> {
+        /**
+         * The column containing the cells to transition to edition state.
+         */
+        private final TableColumn<S,?> column;
+
+        /**
+         * A few special character to recognize in addition to digits.
+         */
+        private char minusSign, zeroDigit;
+
+        /**
+         * The text to initially show in the editor, or {@code null} if none.
+         * If a key has been pressed, then it should be that key.
+         */
+        String initialText;
+
+        /**
+         * Creates a new trigger for transitioning cells in the specified column.
+         *
+         * @param  column  the column containing the cells to transition to edition state.
+         * @param  format  the format used for formatting values.
+         */
+        public Trigger(final TableColumn<S,?> column, final Format format) {
+            this.column = column;
+            if (format instanceof DecimalFormat) {
+                final DecimalFormatSymbols symbols = ((DecimalFormat) format).getDecimalFormatSymbols();
+                minusSign = symbols.getMinusSign();
+                zeroDigit = symbols.getZeroDigit();
+            }
+        }
+
+        /**
+         * Registers this trigger to the given table. This method registers also a listener
on the
+         * F2 key for editing cell on the current row instead than cell in the insertion
row.
+         * It assumes that only one column should get the focus when F2 is pressed,
+         * and that column is the one given in constructor to this {@code Trigger}.
+         *
+         * @param  target  table where to register listeners.
+         */
+        public void registerTo(final TableView<S> target) {
+            target.addEventHandler(KeyEvent.KEY_TYPED, this);
+            target.addEventHandler(KeyEvent.KEY_PRESSED, (event) -> {
+                if (event.getCode() == KeyCode.F2) {
+                    final TableView<S> table = column.getTableView();
+                    if (table.getEditingCell() == null) {
+                        final TablePosition<?,?> cell = table.getFocusModel().getFocusedCell();
+                        if (cell != null) {
+                            table.edit(cell.getRow(), column);
+                        }
+                    }
+                    event.consume();
+                }
+            });
+        }
+
+        /**
+         * Invoked when user typed a key. If the key is one of the keys used for entering
numbers
+         * and if no edition is already under way, transition to edition state on the last
row.
+         * This method is public as an implementation side-effect and should not be invoked
directly.
+         *
+         * @param  event  event that describe the key typed.
+         */
+        @Override
+        public void handle(final KeyEvent event) {
+            final TableView<S> table = column.getTableView();
+            if (table.getEditingCell() == null) {
+                final String t = event.getCharacter();
+                if (t.length() == 1) {
+                    final char c = t.charAt(0);
+                    if ((c >= '0' && c <= '9') || c == minusSign || c == zeroDigit)
{
+                        final int row = table.getItems().size() - 1;
+                        try {
+                            initialText = t;
+                            table.edit(row, column);
+                        } finally {
+                            initialText = null;
+                        }
+                        event.consume();
+                    }
+                }
+            }
+        }
+    }
 }


Mime
View raw message