sis-commits mailing list archives

Site index · List index
Message view « Date » · « Thread »
Top « Date » · « Thread »
From desruisse...@apache.org
Subject [sis] 03/03: Move part of `CategoryColorCell` into a new `ColorCell` base class. The intent is to share this code with other kinds of color tables, to be created later for isolines.
Date Thu, 31 Dec 2020 16:17:25 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 d33c28c5be386f48ef748567fcae7ff04856d442
Author: Martin Desruisseaux <martin.desruisseaux@geomatys.com>
AuthorDate: Thu Dec 31 17:14:18 2020 +0100

    Move part of `CategoryColorCell` into a new `ColorCell` base class.
    The intent is to share this code with other kinds of color tables,
    to be created later for isolines.
---
 .../apache/sis/gui/coverage/CategoryColors.java    |  27 +-
 .../sis/gui/coverage/CategoryColorsCell.java       | 241 +++-----------
 .../apache/sis/internal/gui/control/ColorCell.java | 350 +++++++++++++++++++++
 .../sis/internal/gui/control/package-info.java     |  31 ++
 4 files changed, 452 insertions(+), 197 deletions(-)

diff --git a/application/sis-javafx/src/main/java/org/apache/sis/gui/coverage/CategoryColors.java
b/application/sis-javafx/src/main/java/org/apache/sis/gui/coverage/CategoryColors.java
index c8f122b..adf2e56 100644
--- a/application/sis-javafx/src/main/java/org/apache/sis/gui/coverage/CategoryColors.java
+++ b/application/sis-javafx/src/main/java/org/apache/sis/gui/coverage/CategoryColors.java
@@ -18,6 +18,7 @@ package org.apache.sis.gui.coverage;
 
 import java.util.Arrays;
 import javafx.collections.ObservableList;
+import javafx.scene.Node;
 import javafx.scene.control.ComboBox;
 import javafx.scene.paint.Color;
 import javafx.scene.paint.CycleMethod;
@@ -25,8 +26,9 @@ import javafx.scene.paint.LinearGradient;
 import javafx.scene.paint.Paint;
 import javafx.scene.paint.Stop;
 import org.apache.sis.internal.gui.ColorName;
-import org.apache.sis.util.resources.Vocabulary;
 import org.apache.sis.internal.gui.GUIUtilities;
+import org.apache.sis.internal.gui.control.ColorCell;
+import org.apache.sis.util.resources.Vocabulary;
 
 
 /**
@@ -37,7 +39,7 @@ import org.apache.sis.internal.gui.GUIUtilities;
  * @since   1.1
  * @module
  */
-final class CategoryColors {
+final class CategoryColors extends ColorCell.Item {
     /**
      * Default color palette.
      */
@@ -74,6 +76,21 @@ final class CategoryColors {
     }
 
     /**
+     * Updates a control with the current color of this view.
+     *
+     * @return whether the given control has been recognized.
+     */
+    @Override
+    @SuppressWarnings("unchecked")
+    protected boolean updateControl(final Node control) {
+        if (!super.updateControl(control)) {
+            // A ClassCastException here would be a bug in CategoryColorsCell editors management.
+            asSelectedItem((ComboBox<CategoryColors>) control);
+        }
+        return true;
+    }
+
+    /**
      * Declares this {@code CategoryColors} as the selected item in the given chooser.
      * If this instance is not found, then it is added to the chooser list.
      */
@@ -91,7 +108,8 @@ final class CategoryColors {
      * Returns the first color, or {@code null} if none.
      * This is used for qualitative categories, which are expected to contain only one color.
      */
-    final Color firstColor() {
+    @Override
+    protected final Color color() {
         if (colors != null && colors.length != 0) {
             return GUIUtilities.fromARGB(colors[0]);
         } else {
@@ -103,7 +121,8 @@ final class CategoryColors {
      * Gets the paint to use for filling a rectangle using this color palette.
      * Returns {@code null} if this {@code CategoryColors} contains no color.
      */
-    final Paint paint() {
+    @Override
+    protected final Paint paint() {
         if (paint == null) {
             switch (colors.length) {
                 case 0: break;
diff --git a/application/sis-javafx/src/main/java/org/apache/sis/gui/coverage/CategoryColorsCell.java
b/application/sis-javafx/src/main/java/org/apache/sis/gui/coverage/CategoryColorsCell.java
index c223341..d35cd29 100644
--- a/application/sis-javafx/src/main/java/org/apache/sis/gui/coverage/CategoryColorsCell.java
+++ b/application/sis-javafx/src/main/java/org/apache/sis/gui/coverage/CategoryColorsCell.java
@@ -16,9 +16,7 @@
  */
 package org.apache.sis.gui.coverage;
 
-import javafx.scene.Node;
 import javafx.scene.paint.Color;
-import javafx.scene.paint.Paint;
 import javafx.scene.shape.Rectangle;
 import javafx.scene.control.ComboBox;
 import javafx.scene.control.ComboBoxBase;
@@ -26,16 +24,14 @@ import javafx.scene.control.ListCell;
 import javafx.scene.control.TableCell;
 import javafx.scene.control.TableView;
 import javafx.scene.control.TableColumn;
-import javafx.scene.control.ColorPicker;
 import javafx.scene.control.ContentDisplay;
 import javafx.geometry.Pos;
-import javafx.event.ActionEvent;
-import javafx.event.EventHandler;
 import javafx.beans.value.ObservableValue;
 import javafx.beans.property.ReadOnlyObjectWrapper;
 import org.opengis.util.InternationalString;
 import org.apache.sis.util.resources.Vocabulary;
 import org.apache.sis.internal.gui.GUIUtilities;
+import org.apache.sis.internal.gui.control.ColorCell;
 import org.apache.sis.coverage.Category;
 
 
@@ -51,17 +47,7 @@ import org.apache.sis.coverage.Category;
  * @since   1.1
  * @module
  */
-final class CategoryColorsCell extends TableCell<Category,CategoryColors> implements
EventHandler<ActionEvent> {
-    /**
-     * Space (in pixels) to remove on right side of the rectangle representing colors.
-     */
-    private static final double WIDTH_ADJUST = -9;
-
-    /**
-     * Height (in pixels) of the rectangle representing colors.
-     */
-    private static final double HEIGHT = 16;
-
+final class CategoryColorsCell extends ColorCell<Category,CategoryColors> {
     /**
      * The function that determines which colors to apply on a given category.
      * This same instance is shared by all cells of the same category table.
@@ -69,111 +55,83 @@ final class CategoryColorsCell extends TableCell<Category,CategoryColors>
implem
     private final CoverageStyling styling;
 
     /**
-     * The control for selecting a single color, or {@code null} if not yet created.
-     * This applies to qualitative categories.
-     */
-    private ColorPicker colorPicker;
-
-    /**
      * The control for selecting a color ramp, or {@code null} if not yet created.
-     * This applies to quantitative categories.
+     * This applies to quantitative categories. By contrast, {@link #colorPicker}
+     * applies to qualitative categories.
      *
      * @see Category#isQuantitative()
      */
     private ComboBox<CategoryColors> colorRampChooser;
 
     /**
-     * The single color shown in the table. Created when first needed.
-     */
-    private Rectangle singleColor;
-
-    /**
-     * Colors to restore if user cancels an edit action.
-     */
-    private CategoryColors restoreOnCancel;
-
-    /**
      * Creates a cell for the colors column.
      * This constructor is for {@link #createTable(CoverageStyling, Vocabulary)} usage only.
      */
     private CategoryColorsCell(final CoverageStyling styling) {
         this.styling = styling;
-        setContentDisplay(ContentDisplay.GRAPHIC_ONLY);
     }
 
     /**
-     * Invoked when the color in this cell changed. It may be because of user selection in
the combo box,
-     * or because this cell is now used for a new {@link Category} instance.
-     *
-     * <div class="note"><b>Implementation note:</b>
-     * this method should not invoke {@link #setGraphic(Node)} if the current graphic is
a {@link ComboBoxBase}
-     * because this method may be invoked at any time, including during execution of {@link
#startEdit()} or
-     * {@link #commitEdit(Object)}. Adding or removing {@link ColorPicker} or {@link ComboBox}
in this method
-     * cause problems with focus system. In particular we must be sure to remove {@link ColorPicker}
only after
-     * it has lost focus.</div>
+     * Returns the initial item for a new cell.
      */
     @Override
-    @SuppressWarnings("unchecked")
-    protected void updateItem(final CategoryColors colors, final boolean empty) {
-        super.updateItem(colors, empty);
-        final Node control = getGraphic();
-        if (colors != null) {
-            if (control == null) {
-                setColorView(colors);
-            } else if (control instanceof Rectangle) {
-                ((Rectangle) control).setFill(colors.paint());
-            } else if (control instanceof ColorPicker) {
-                ((ColorPicker) control).setValue(colors.firstColor());
-            } else {
-                // A ClassCastException here would be a bug in CategoryColorsCell editors
management.
-                colors.asSelectedItem(((ComboBox<CategoryColors>) control));
-            }
-        } else if (control instanceof Rectangle) {
-            setGraphic(null);
-        }
+    protected CategoryColors getDefaultItem() {
+        return CategoryColors.GRAYSCALE;
     }
 
     /**
-     * Returns {@code true} if neither {@link #colorPicker} or {@link #colorRampChooser}
has the focus.
-     * This is used for assertions.
+     * Creates an item for a new color selected by user.
+     * The given object may be an instance of one of the following classes:
+     *
+     * <ul>
+     *   <li>{@link Color} if the chooser was {@link javafx.scene.control.ColorPicker}.</li>
+     *   <li>{@link CategoryColors} if the chooser was {@code ComboBox<CategoryColors>}.</li>
+     * </ul>
+     *
+     * @param  value  the color or gradient paint selected by the user.
+     * @return the item to store in this cell for the given color or gradient.
      */
-    private boolean controlNotFocused() {
-        return (colorPicker == null || !colorPicker.isFocused()) &&
-                (colorRampChooser == null || !colorRampChooser.isFocused());
+    @Override
+    protected CategoryColors createItemForSelection(final Object value) {
+        if (value instanceof Color) {
+            return new CategoryColors(GUIUtilities.toARGB((Color) value));
+        } else {
+            // A ClassCastException here would be a bug in ColorsCell editors management.
+            return (CategoryColors) value;
+        }
     }
 
     /**
-     * Sets the color representation when no editing is under way. It is caller responsibility
to ensure
-     * that the current graphic is not a combo box, or that it is safe to remove that combo
box from the
-     * scene (i.e. that combo box does not have focus anymore).
+     * Returns a color or gradient chooser initialized to the given item.
+     * This is invoked when the user clicks on a cell for modifying the color value.
+     *
+     * @param  colors  the initial color or gradient to show.
+     * @return the control to show to user.
      */
-    private void setColorView(final CategoryColors colors) {
-        assert controlNotFocused();
-        Rectangle view = null;
-        if (colors != null) {
-            final Paint paint = colors.paint();
-            if (paint != null) {
-                if (singleColor == null) {
-                    singleColor = createRectangle(WIDTH_ADJUST);
-                }
-                view = singleColor;
-                view.setFill(paint);
+    @Override
+    protected ComboBoxBase<?> getControlForEdit(final CategoryColors colors) {
+        if (getTableRow().getItem().isQuantitative()) {
+            if (colorRampChooser == null) {
+                colorRampChooser = new ComboBox<>();
+                colorRampChooser.setEditable(false);
+                colorRampChooser.setCellFactory((column) -> new Ramp());
+                colorRampChooser.getItems().setAll(CategoryColors.GRAYSCALE, CategoryColors.BELL);
+                addListeners(colorRampChooser);
             }
+            colors.asSelectedItem(colorRampChooser);
+            return colorRampChooser;
+        } else {
+            return super.getControlForEdit(colors);
         }
-        setGraphic(view);
     }
 
     /**
-     * Creates the graphic to draw in a table cell or combo box cell for representing a color
or color ramp.
-     *
-     * @param  adjust  amount of space (in pixels) to add or remove on the right size.
-     *                 Should be a negative number for removing space.
+     * Returns {@code true} if neither {@link #colorPicker} or {@link #colorRampChooser}
has the focus.
+     * This is used for assertions.
      */
-    private Rectangle createRectangle(final double adjust) {
-        final Rectangle gr = new Rectangle();
-        gr.setHeight(HEIGHT);
-        gr.widthProperty().bind(widthProperty().add(adjust));
-        return gr;
+    @Override
+    protected boolean controlNotFocused() {
+        return super.controlNotFocused() && (colorRampChooser == null || !colorRampChooser.isFocused());
     }
 
     /**
@@ -204,89 +162,6 @@ final class CategoryColorsCell extends TableCell<Category,CategoryColors>
implem
     }
 
     /**
-     * Transitions from non-editing state to editing state. This method is automatically
invoked when a
-     * row is selected and the user clicks on the color cell in that row. This method sets
the combo box
-     * as the graphic element in that cell and shows it immediately. The immediate {@code
control.show()}
-     * is for avoiding to force users to perform a third mouse click.
-     */
-    @Override
-    public void startEdit() {
-        restoreOnCancel = getItem();
-        final CategoryColors colors = (restoreOnCancel != null) ? restoreOnCancel : CategoryColors.GRAYSCALE;
-        final ComboBoxBase<?> control;
-        if (getTableRow().getItem().isQuantitative()) {
-            if (colorRampChooser == null) {
-                colorRampChooser = new ComboBox<>();
-                colorRampChooser.setEditable(false);
-                colorRampChooser.setCellFactory((column) -> new Ramp());
-                colorRampChooser.getItems().setAll(CategoryColors.GRAYSCALE, CategoryColors.BELL);
-                addListeners(colorRampChooser);
-            }
-            colors.asSelectedItem(colorRampChooser);
-            control = colorRampChooser;
-        } else {
-            if (colorPicker == null) {
-                colorPicker = new ColorPicker();
-                addListeners(colorPicker);
-            }
-            colorPicker.setValue(colors.firstColor());
-            control = colorPicker;
-        }
-        /*
-         * Call `startEdit()` only after above call to `setValue(…)` because we want `isEditing()`
-         * to return false during above value change. This is for preventing change listeners
to
-         * misinterpret the value change as a user selection.
-         */
-        super.startEdit();
-        setGraphic(control);            // Must be before `requestFocus()`, otherwise focus
request is ignored.
-        control.requestFocus();         // Must be before `show()`, otherwise there is apparent
focus confusion.
-        control.show();
-    }
-
-    /**
-     * Finishes configuration of a newly created combo box.
-     */
-    private void addListeners(final ComboBoxBase<?> control) {
-        control.setOnAction(this);
-        control.setOnHidden((e) -> hidden());
-    }
-
-    /**
-     * Invoked when a combo box has been hidden. This method sets the focus to the table
before to remove
-     * the combo box. This is necessary for causing the combo box to lost focus, otherwise
focus problems
-     * appear next time that the combo box is shown.
-     *
-     * <p>IF the cell was in editing mode when this method is invoked, it means that
the user clicked outside
-     * the combo box area without validating his/her choice. In this case {@link #commitEdit(Object)}
has not
-     * been invoked and we need to either commit now or cancel. Current implementation cancels.</p>
-     */
-    private void hidden() {
-        if (isEditing()) {
-            if (isHover()) {
-                return;                 // Keep editing state.
-            }
-            setItem(restoreOnCancel);
-            super.cancelEdit();
-        }
-        restoreOnCancel = null;
-        getTableView().requestFocus();  // Must be before `setGraphic(…)` for causing ColorPicker
to lost focus.
-        setColorView(getItem());
-    }
-
-    /**
-     * Transitions from an editing state into a non-editing state without saving any user
input.
-     * This method is automatically invoked when the user click on another table row.
-     */
-    @Override
-    public void cancelEdit() {
-        setItem(restoreOnCancel);
-        restoreOnCancel = null;
-        super.cancelEdit();
-        assert controlNotFocused();
-        setColorView(getItem());
-    }
-
-    /**
      * Invoked when users confirmed that (s)he wants to use the selected colors.
      *
      * @param  colors  the colors to use.
@@ -298,26 +173,6 @@ final class CategoryColorsCell extends TableCell<Category,CategoryColors>
implem
     }
 
     /**
-     * Invoked when the user selected a new value in the color picker or color ramp chooser.
-     */
-    @Override
-    @SuppressWarnings("unchecked")
-    public void handle(final ActionEvent event) {
-        if (isEditing()) {
-            final Object source = event.getSource();
-            final CategoryColors value;
-            if (source instanceof ColorPicker) {
-                final Color color = ((ColorPicker) source).getValue();
-                value = (color != null) ? new CategoryColors(GUIUtilities.toARGB(color))
: null;
-            } else {
-                // A ClassCastException here would be a bug in CategoryColorsCell editors
management.
-                value = ((ComboBox<CategoryColors>) source).getValue();
-            }
-            commitEdit(value);
-        }
-    }
-
-    /**
      * Creates a table of categories.
      *
      * @param  styling     function that determines which colors to apply on a given category.
diff --git a/application/sis-javafx/src/main/java/org/apache/sis/internal/gui/control/ColorCell.java
b/application/sis-javafx/src/main/java/org/apache/sis/internal/gui/control/ColorCell.java
new file mode 100644
index 0000000..03cabba
--- /dev/null
+++ b/application/sis-javafx/src/main/java/org/apache/sis/internal/gui/control/ColorCell.java
@@ -0,0 +1,350 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.sis.internal.gui.control;
+
+import javafx.event.ActionEvent;
+import javafx.event.EventHandler;
+import javafx.scene.Node;
+import javafx.scene.control.ColorPicker;
+import javafx.scene.control.ComboBoxBase;
+import javafx.scene.control.ContentDisplay;
+import javafx.scene.control.TableCell;
+import javafx.scene.paint.Color;
+import javafx.scene.paint.Paint;
+import javafx.scene.shape.Rectangle;
+
+
+/**
+ * Cell representing the color of an object.
+ * The color can be modified by selecting the table row, then clicking on the color.
+ * Subclasses should override the following methods:
+ *
+ * <ul>
+ *   <li>{@link #getDefaultItem()}</li>
+ *   <li>{@link #createItemForSelection(Object)}</li>
+ *   <li>{@link #getControlForEdit(T)} (unless showing only solid colors)</li>
+ *   <li>{@link #controlNotFocused()} (unless showing only solid colors)</li>
+ *   <li>{@link #commitEdit(Object)}</li>
+ * </ul>
+ *
+ * All methods are invoked in JavaFX thread.
+ *
+ * <p>The interfaces implemented by this class are implementation convenience
+ * that may change in any future version.</p>
+ *
+ * @author  Martin Desruisseaux (Geomatys)
+ * @version 1.1
+ *
+ * @param  <S>  the type of the {@code TableView} generic type.
+ * @param  <T>  the type of the item contained within the cell.
+ *
+ * @since 1.1
+ * @module
+ */
+public abstract class ColorCell<S,T extends ColorCell.Item> extends TableCell<S,T>
implements EventHandler<ActionEvent> {
+    /**
+     * Gradient paint, colors or string representation of the rectangle to show in {@link
ColorCell}.
+     * This is the object stored in {@link TableCell} after conversion from user value of
type {@code <S>}.
+     *
+     * @see TableCell#getItem()
+     */
+    public abstract static class Item {
+        /**
+         * Returns the paint to use for filling a rectangle in {@link ColorCell}, or {@code
null} if none.
+         * The default implementation returns the solid {@linkplain #color() color} (no gradient).
+         *
+         * @return color or gradient paint for table cell, or {@code null} if none.
+         */
+        protected Paint paint() {
+            return color();
+        }
+
+        /**
+         * Returns a solid color to use for filling a rectangle in {@link ColorCell}.
+         * If this view has many colors (for example because it uses a gradient),
+         * then some representative color or an arbitrary color should be returned.
+         *
+         * @return color for table cell, or {@code null} if none.
+         */
+        protected abstract Color color();
+
+        /**
+         * Updates a control with the current color of this item. Default implementation
+         * recognizes {@link ColorPicker}. Subclasses should override if different kinds
+         * of controls need to be handled.
+         *
+         * @param  control  the control to update.
+         * @return whether the given control has been recognized.
+         */
+        protected boolean updateControl(final Node control) {
+            if (control instanceof Rectangle) {
+                ((Rectangle) control).setFill(paint());
+            } else if (control instanceof ColorPicker) {
+                ((ColorPicker) control).setValue(color());
+            } else {
+                return false;
+            }
+            return true;
+        }
+    }
+
+    /**
+     * Space (in pixels) to remove on right side of the rectangle showing colors.
+     */
+    private static final double WIDTH_ADJUST = -9;
+
+    /**
+     * Height (in pixels) of the rectangle showing colors.
+     */
+    private static final double HEIGHT = 16;
+
+    /**
+     * The control for selecting a single color, or {@code null} if not yet created.
+     */
+    private ColorPicker colorPicker;
+
+    /**
+     * The single color shown in a cell of the "color" column. Created when first needed.
+     * User can modify the color of this rectangle with {@link #colorPicker} or other control
+     * provided by {@link #getControlForEdit(T)}.
+     */
+    private Rectangle singleColor;
+
+    /**
+     * Colors to restore if user cancels an edit action.
+     */
+    private T restoreOnCancel;
+
+    /**
+     * Creates a new cell for the colors column.
+     */
+    protected ColorCell() {
+        setContentDisplay(ContentDisplay.GRAPHIC_ONLY);
+    }
+
+    /**
+     * Returns the initial item for a new cell. This is invoked when the
+     * user wants to edit a cell but {@link #getItem()} is still null.
+     *
+     * @return initial color or paint for a new cell.
+     */
+    protected abstract T getDefaultItem();
+
+    /**
+     * Creates an item for a new color selected using {@link ColorPicker} or other chooser.
+     * The given object may be an instance of one of the following classes:
+     *
+     * <ul>
+     *   <li>{@link Color} if the chooser was {@link ColorPicker}.</li>
+     *   <li>{@code <T>} if the chooser was {@code ComboBox<T>}.</li>
+     *   <li>Any other kind of value depending on {@link #getControlForEdit(T)}.</li>
+     * </ul>
+     *
+     * @param  value  the color or gradient paint selected by the user.
+     * @return the item to store in this cell for the given color or gradient.
+     */
+    protected abstract T createItemForSelection(Object value);
+
+    /**
+     * Returns a color or gradient chooser initialized to the given item.
+     * This is invoked when the user clicks on a cell for modifying the color value.
+     * The default implementation returns a {@link ColorPicker}.
+     *
+     * @param  colors  the initial color or gradient to show.
+     * @return the control to show to user.
+     */
+    protected ComboBoxBase<?> getControlForEdit(final T colors) {
+        if (colorPicker == null) {
+            colorPicker = new ColorPicker();
+            addListeners(colorPicker);
+        }
+        colorPicker.setValue(colors.color());
+        return colorPicker;
+    }
+
+    /**
+     * Returns {@code true} if the {@link #colorPicker} does not have the focus.
+     * This is used for assertions. Subclasses should override if there is more
+     * controls that may have the focus.
+     *
+     * @return {@code true} if no control has the focus.
+     */
+    protected boolean controlNotFocused() {
+        return (colorPicker == null) || !colorPicker.isFocused();
+    }
+
+    /**
+     * Creates the graphic to draw in a table cell or combo box cell for representing a color
or color ramp.
+     * This method may be invoked by subclasses for building an editor in {@link #getControlForEdit(T)}.
+     *
+     * @param  adjust  amount of space (in pixels) to add on the right size.
+     *                 Can be a negative number for removing space.
+     * @return graphic to draw in a table cell or combo box cell.
+     */
+    protected final Rectangle createRectangle(final double adjust) {
+        final Rectangle gr = new Rectangle();
+        gr.setHeight(HEIGHT);
+        gr.widthProperty().bind(widthProperty().add(adjust));
+        return gr;
+    }
+
+    /**
+     * Finishes configuration of a newly created combo box.
+     * This method may be invoked by subclasses for building an editor in {@link #getControlForEdit(T)}.
+     *
+     * @param  control  the {@link ColorPicker} or other combo box on which to add listeners.
+     */
+    protected final void addListeners(final ComboBoxBase<?> control) {
+        control.setOnAction(this);
+        control.setOnHidden((e) -> hidden());
+    }
+
+
+    //
+    // Methods below this line should not be called or overridden by subclasses.
+    //
+
+
+    /**
+     * Invoked when the color in this cell changed. It may be because of user selection in
a combo box,
+     * or because this cell is now used for a new {@code <S>} instance.
+     *
+     * <div class="note"><b>Implementation note:</b>
+     * this method should not invoke {@link #setGraphic(Node)} if the current graphic is
a {@link ComboBoxBase}
+     * (the parent of {@link ColorPicker}) because this method may be invoked at any time,
including during the
+     * execution of {@link #startEdit()} or {@link #commitEdit(Object)} methods.
+     * Adding or removing {@link ComboBoxBase} in this method cause problems with focus system.
+     * In particular we must be sure to remove {@link ColorPicker} only after it has lost
focus.</div>
+     *
+     * @param  colors  the new object to show as a color or gradient in this cell.
+     * @param  empty   {@code true} if this method is invoked for creating an empty cell.
+     */
+    @Override
+    protected final void updateItem(final T colors, final boolean empty) {
+        super.updateItem(colors, empty);
+        final Node control = getGraphic();
+        if (colors != null) {
+            if (control == null) {
+                setColorItem(colors);
+            } else {
+                colors.updateControl(control);
+            }
+        } else if (control instanceof Rectangle) {
+            setGraphic(null);
+        }
+    }
+
+    /**
+     * Sets the color representation when no editing is under way. It is caller responsibility
to ensure
+     * that the current graphic is not a combo box, or that it is safe to remove that combo
box from the
+     * scene (i.e. that combo box does not have focus anymore).
+     *
+     * @param  colors  current value of {@link #getItem()}.
+     */
+    private void setColorItem(final T colors) {
+        assert controlNotFocused();
+        Rectangle view = null;
+        if (colors != null) {
+            final Paint paint = colors.paint();
+            if (paint != null) {
+                if (singleColor == null) {
+                    singleColor = createRectangle(WIDTH_ADJUST);
+                }
+                view = singleColor;
+                view.setFill(paint);
+            }
+        }
+        setGraphic(view);
+    }
+
+    /**
+     * Transitions from non-editing state to editing state. This method is automatically
invoked when a
+     * row is selected and the user clicks on the color cell in that row. This method sets
the combo box
+     * as the graphic element in that cell and shows it immediately. The immediate {@code
control.show()}
+     * is for avoiding to force users to perform a third mouse click.
+     */
+    @Override
+    public final void startEdit() {
+        restoreOnCancel = getItem();
+        final T colors = (restoreOnCancel != null) ? restoreOnCancel : getDefaultItem();
+        final ComboBoxBase<?> control = getControlForEdit(colors);
+        /*
+         * Call `startEdit()` only after above call to `setValue(…)` because we want `isEditing()`
+         * to return false during above value change. This is for preventing change listeners
to
+         * misinterpret the value change as a user selection.
+         */
+        super.startEdit();
+        setGraphic(control);            // Must be before `requestFocus()`, otherwise focus
request is ignored.
+        control.requestFocus();         // Must be before `show()`, otherwise there is apparent
focus confusion.
+        control.show();
+    }
+
+    /**
+     * Transitions from an editing state into a non-editing state without saving any user
input.
+     * This method is automatically invoked when the user click on another table row.
+     */
+    @Override
+    public final void cancelEdit() {
+        setItem(restoreOnCancel);
+        restoreOnCancel = null;
+        super.cancelEdit();
+        assert controlNotFocused();
+        setColorItem(getItem());
+    }
+
+    /**
+     * Invoked when a combo box has been hidden. This method sets the focus to the table
before to remove
+     * the combo box. This is necessary for causing the combo box to lost focus, otherwise
focus problems
+     * appear next time that the combo box is shown.
+     *
+     * <p>If the cell was in editing mode when this method is invoked, it means that
the user clicked outside
+     * the combo box area without validating his/her choice. In this case {@link #commitEdit(Object)}
has not
+     * been invoked and we need to either commit now or cancel. Current implementation cancels.</p>
+     */
+    private void hidden() {
+        if (isEditing()) {
+            if (isHover()) {
+                return;                 // Keep editing state.
+            }
+            setItem(restoreOnCancel);
+            super.cancelEdit();
+        }
+        restoreOnCancel = null;
+        getTableView().requestFocus();  // Must be before `setGraphic(…)` for causing ColorPicker
to lost focus.
+        setColorItem(getItem());
+    }
+
+    /**
+     * Invoked when the user selected a new value in the color picker or color ramp chooser.
+     * This method is public as an implementation side-effect and should never be invoked
directly.
+     *
+     * @param  event  the {@link ComboBoxBase} on which a selection occurred.
+     */
+    @Override
+    public final void handle(final ActionEvent event) {
+        if (isEditing()) {
+            final Object source = event.getSource();
+            final T value;
+            if (source instanceof ComboBoxBase<?>) {
+                value = createItemForSelection(((ComboBoxBase<?>) source).getValue());
+            } else {
+                value = restoreOnCancel;
+            }
+            commitEdit(value);
+        }
+    }
+}
diff --git a/application/sis-javafx/src/main/java/org/apache/sis/internal/gui/control/package-info.java
b/application/sis-javafx/src/main/java/org/apache/sis/internal/gui/control/package-info.java
new file mode 100644
index 0000000..7366f07
--- /dev/null
+++ b/application/sis-javafx/src/main/java/org/apache/sis/internal/gui/control/package-info.java
@@ -0,0 +1,31 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+/**
+ * Classes related to JavaFX controls.
+ *
+ * <STRONG>Do not use!</STRONG>
+ *
+ * This package is for internal use by SIS only. Classes in this package
+ * may change in incompatible ways in any future version without notice.
+ *
+ * @author  Martin Desruisseaux (Geomatys)
+ * @version 1.1
+ * @since   1.1
+ * @module
+ */
+package org.apache.sis.internal.gui.control;


Mime
View raw message