sis-commits mailing list archives

Site index · List index
Message view « Date » · « Thread »
Top « Date » · « Thread »
From desruisse...@apache.org
Subject [sis] 01/04: Move remaining code from `CategoryColorsCell` into `ColorCell` for sharing (in a future commit) with isoline table. The methods that were previously overridden in `CategoryColorsCell` class are now overridden in `ColorColumnHandler`. Cell behavior has been reworked for reducing the amount of mouse clicks and for enabling keyboard usage.
Date Thu, 07 Jan 2021 18:08:30 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 da676c5da6a88495fa87b36e6d0921c40692a5e5
Author: Martin Desruisseaux <martin.desruisseaux@geomatys.com>
AuthorDate: Mon Jan 4 16:23:32 2021 +0100

    Move remaining code from `CategoryColorsCell` into `ColorCell` for sharing (in a future commit) with isoline table.
    The methods that were previously overridden in `CategoryColorsCell` class are now overridden in `ColorColumnHandler`.
    Cell behavior has been reworked for reducing the amount of mouse clicks and for enabling keyboard usage.
---
 .../sis/gui/coverage/CategoryColorsCell.java       | 220 -----------
 .../apache/sis/gui/coverage/CoverageControls.java  |   2 +-
 .../apache/sis/gui/coverage/CoverageStyling.java   |  85 ++++-
 .../sis/internal/gui/ImmutableObjectProperty.java  |   5 +-
 .../apache/sis/internal/gui/control/ColorCell.java | 421 +++++++++++++--------
 .../internal/gui/control/ColorColumnHandler.java   | 124 ++++++
 .../gui/control/ColorRamp.java}                    | 131 ++++---
 .../sis/gui/coverage/CoverageStylingApp.java       |   6 +-
 8 files changed, 529 insertions(+), 465 deletions(-)

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
deleted file mode 100644
index d35cd29..0000000
--- a/application/sis-javafx/src/main/java/org/apache/sis/gui/coverage/CategoryColorsCell.java
+++ /dev/null
@@ -1,220 +0,0 @@
-/*
- * 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.gui.coverage;
-
-import javafx.scene.paint.Color;
-import javafx.scene.shape.Rectangle;
-import javafx.scene.control.ComboBox;
-import javafx.scene.control.ComboBoxBase;
-import javafx.scene.control.ListCell;
-import javafx.scene.control.TableCell;
-import javafx.scene.control.TableView;
-import javafx.scene.control.TableColumn;
-import javafx.scene.control.ContentDisplay;
-import javafx.geometry.Pos;
-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;
-
-
-/**
- * Cell representing the color of a qualitative or quantitative category.
- * The color can be modified by selecting the table row, then clicking on the color.
- *
- * <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
- * @since   1.1
- * @module
- */
-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.
-     */
-    private final CoverageStyling styling;
-
-    /**
-     * The control for selecting a color ramp, or {@code null} if not yet created.
-     * This applies to quantitative categories. By contrast, {@link #colorPicker}
-     * applies to qualitative categories.
-     *
-     * @see Category#isQuantitative()
-     */
-    private ComboBox<CategoryColors> colorRampChooser;
-
-    /**
-     * 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;
-    }
-
-    /**
-     * Returns the initial item for a new cell.
-     */
-    @Override
-    protected CategoryColors getDefaultItem() {
-        return CategoryColors.GRAYSCALE;
-    }
-
-    /**
-     * 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.
-     */
-    @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;
-        }
-    }
-
-    /**
-     * 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.
-     */
-    @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);
-        }
-    }
-
-    /**
-     * Returns {@code true} if neither {@link #colorPicker} or {@link #colorRampChooser} has the focus.
-     * This is used for assertions.
-     */
-    @Override
-    protected boolean controlNotFocused() {
-        return super.controlNotFocused() && (colorRampChooser == null || !colorRampChooser.isFocused());
-    }
-
-    /**
-     * Cell for a color ramp in a {@link ComboBox}.
-     */
-    private final class Ramp extends ListCell<CategoryColors> {
-        /** Creates a new cell. */
-        Ramp() {
-            setContentDisplay(ContentDisplay.GRAPHIC_ONLY);
-            setMaxWidth(Double.POSITIVE_INFINITY);
-        }
-
-        /** Sets the colors to show in the combo box item. */
-        @Override
-        protected void updateItem(final CategoryColors colors, final boolean empty) {
-            super.updateItem(colors, empty);
-            if (colors == null) {
-                setGraphic(null);
-            } else {
-                Rectangle r = (Rectangle) getGraphic();
-                if (r == null) {
-                    r = createRectangle(-40);
-                    setGraphic(r);
-                }
-                r.setFill(colors.paint());
-            }
-        }
-    }
-
-    /**
-     * Invoked when users confirmed that (s)he wants to use the selected colors.
-     *
-     * @param  colors  the colors to use.
-     */
-    @Override
-    public void commitEdit(final CategoryColors colors) {
-        super.commitEdit(colors);
-        styling.setARGB(getTableRow().getItem(), colors.colors);
-    }
-
-    /**
-     * Creates a table of categories.
-     *
-     * @param  styling     function that determines which colors to apply on a given category.
-     * @param  vocabulary  resources for the locale in use.
-     */
-    static TableView<Category> createTable(final CoverageStyling styling, final Vocabulary vocabulary) {
-        final TableColumn<Category,String> name = new TableColumn<>(vocabulary.getString(Vocabulary.Keys.Name));
-        name.setCellValueFactory(CategoryColorsCell::getCategoryName);
-        name.setCellFactory(CategoryColorsCell::createNameCell);
-        name.setEditable(false);
-        name.setId("name");
-
-        final TableColumn<Category,CategoryColors> colors = new TableColumn<>(vocabulary.getString(Vocabulary.Keys.Colors));
-        colors.setCellValueFactory(styling);
-        colors.setCellFactory((column) -> new CategoryColorsCell(styling));
-        colors.setId("colors");
-        colors.setSortable(false);
-
-        final TableView<Category> table = new TableView<>();
-        table.setColumnResizePolicy(TableView.CONSTRAINED_RESIZE_POLICY);
-        table.getColumns().setAll(name, colors);
-        table.setEditable(true);
-        return table;
-    }
-
-    /**
-     * Invoked for creating a cell for the "name" column.
-     * Returns the JavaFX default cell except for vertical alignment, which is centered.
-     */
-    private static TableCell<Category,String> createNameCell(final TableColumn<Category,String> column) {
-        @SuppressWarnings("unchecked")
-        final TableCell<Category,String> cell =
-                (TableCell<Category,String>) TableColumn.DEFAULT_CELL_FACTORY.call(column);
-        cell.setAlignment(Pos.CENTER_LEFT);
-        return cell;
-    }
-
-    /**
-     * Invoked when the table needs to render a text in the "Name" column of the category table.
-     */
-    private static ObservableValue<String> getCategoryName(final TableColumn.CellDataFeatures<Category,String> cell) {
-        final InternationalString name = cell.getValue().getName();
-        return (name != null) ? new ReadOnlyObjectWrapper<>(name.toString()) : null;
-    }
-}
diff --git a/application/sis-javafx/src/main/java/org/apache/sis/gui/coverage/CoverageControls.java b/application/sis-javafx/src/main/java/org/apache/sis/gui/coverage/CoverageControls.java
index ab2e257..29d0fad 100644
--- a/application/sis-javafx/src/main/java/org/apache/sis/gui/coverage/CoverageControls.java
+++ b/application/sis-javafx/src/main/java/org/apache/sis/gui/coverage/CoverageControls.java
@@ -130,7 +130,7 @@ final class CoverageControls extends Controls {
         final VBox colorsPane;
         {   // Block for making variables locale to this scope.
             final CoverageStyling styling = new CoverageStyling(view);
-            categoryTable = CategoryColorsCell.createTable(styling, vocabulary);
+            categoryTable = styling.createCategoryTable(vocabulary);
             final GridPane gp = Styles.createControlGrid(0,
                 label(vocabulary, Vocabulary.Keys.Stretching, Stretching.createButton((p,o,n) -> view.setStyling(n))));
 
diff --git a/application/sis-javafx/src/main/java/org/apache/sis/gui/coverage/CoverageStyling.java b/application/sis-javafx/src/main/java/org/apache/sis/gui/coverage/CoverageStyling.java
index fca46ed..2e3f7ca 100644
--- a/application/sis-javafx/src/main/java/org/apache/sis/gui/coverage/CoverageStyling.java
+++ b/application/sis-javafx/src/main/java/org/apache/sis/gui/coverage/CoverageStyling.java
@@ -22,12 +22,18 @@ import java.util.Map;
 import java.util.HashMap;
 import java.util.Locale;
 import java.util.function.Function;
-import javafx.util.Callback;
+import javafx.geometry.Pos;
+import javafx.scene.control.TableCell;
 import javafx.scene.control.TableColumn;
+import javafx.scene.control.TableView;
 import javafx.beans.value.ObservableValue;
-import javafx.beans.property.SimpleObjectProperty;
 import org.apache.sis.coverage.Category;
 import org.apache.sis.internal.coverage.j2d.Colorizer;
+import org.apache.sis.internal.gui.ImmutableObjectProperty;
+import org.apache.sis.internal.gui.control.ColorRamp;
+import org.apache.sis.internal.gui.control.ColorColumnHandler;
+import org.apache.sis.util.resources.Vocabulary;
+import org.opengis.util.InternationalString;
 
 
 /**
@@ -41,9 +47,7 @@ import org.apache.sis.internal.coverage.j2d.Colorizer;
  * @since   1.1
  * @module
  */
-final class CoverageStyling implements Function<Category,Color[]>,
-        Callback<TableColumn.CellDataFeatures<Category,CategoryColors>, ObservableValue<CategoryColors>>
-{
+final class CoverageStyling extends ColorColumnHandler<Category> implements Function<Category,Color[]> {
     /**
      * Customized colors selected by user. Keys are English names of categories.
      *
@@ -86,6 +90,8 @@ final class CoverageStyling implements Function<Category,Color[]>,
 
     /**
      * Associates colors to the given category.
+     *
+     * @param  colors  the new color for the given category, or {@code null} for resetting default value.
      */
     final void setARGB(final Category category, final int[] colors) {
         final String key = key(category);
@@ -101,11 +107,13 @@ final class CoverageStyling implements Function<Category,Color[]>,
     }
 
     /**
-     * Same as {@link #apply(Category)}, but returns colors as an array of ARGB codes.
-     * Contrarily to {@link #apply(Category)}, this method may return references to
-     * internal arrays; <strong>do not modify.</strong>
+     * Invoked by {@link TableColumn} for computing value of a {@link ColorCell}.
+     * Does the same work as {@link #apply(Category)}, but returns colors as an array of ARGB codes.
+     * Contrarily to {@link #apply(Category)}, this method may return references to internal arrays;
+     * <strong>do not modify.</strong>
      */
-    private int[] getARGB(final Category category) {
+    @Override
+    protected int[] getARGB(final Category category) {
         int[] ARGB = customizedColors.get(key(category));
         if (ARGB == null) {
             final Color[] colors = fallback.apply(category);
@@ -141,18 +149,55 @@ final class CoverageStyling implements Function<Category,Color[]>,
     }
 
     /**
-     * Invoked by {@link TableColumn} for computing value of a {@link CategoryColorsCell}.
-     * This method is public as an implementation side-effect; do not rely on that.
+     * Invoked when users confirmed that (s)he wants to use the selected colors.
+     *
+     * @param  value    the category for which to assign new color(s).
+     * @param  newItem  the new color for the given category, or {@code null} for resetting default value.
+     * @return the type of color (solid or gradient) shown for the given value.
      */
     @Override
-    public ObservableValue<CategoryColors> call(final TableColumn.CellDataFeatures<Category,CategoryColors> cell) {
-        final Category category = cell.getValue();
-        if (category != null) {
-            final int[] ARGB = getARGB(category);
-            if (ARGB != null) {
-                return new SimpleObjectProperty<>(new CategoryColors(ARGB));
-            }
-        }
-        return null;
+    protected ColorRamp.Type applyColors(final Category value, ColorRamp newItem) {
+        setARGB(value, (newItem != null) ? newItem.colors : null);
+        return value.isQuantitative() ? ColorRamp.Type.GRADIENT : ColorRamp.Type.SOLID;
+    }
+
+    /**
+     * Creates a table showing the color of a qualitative or quantitative coverage categories.
+     * The color can be modified by selecting the table row, then clicking on the color.
+     *
+     * @param  vocabulary  resources for the locale in use.
+     */
+    final TableView<Category> createCategoryTable(final Vocabulary vocabulary) {
+        final TableColumn<Category,String> name = new TableColumn<>(vocabulary.getString(Vocabulary.Keys.Name));
+        name.setCellValueFactory(CoverageStyling::getCategoryName);
+        name.setCellFactory(CoverageStyling::createNameCell);
+        name.setEditable(false);
+        name.setId("name");
+
+        final TableView<Category> table = new TableView<>();
+        table.setColumnResizePolicy(TableView.CONSTRAINED_RESIZE_POLICY);
+        table.getColumns().add(name);
+        addColumnTo(table, vocabulary);
+        table.setEditable(true);
+        return table;
+    }
+
+    /**
+     * Invoked for creating a cell for the "name" column.
+     * Returns the JavaFX default cell except for vertical alignment, which is centered.
+     */
+    private static TableCell<Category,String> createNameCell(final TableColumn<Category,String> column) {
+        @SuppressWarnings("unchecked")
+        final TableCell<Category,String> cell = (TableCell<Category,String>) TableColumn.DEFAULT_CELL_FACTORY.call(column);
+        cell.setAlignment(Pos.CENTER_LEFT);
+        return cell;
+    }
+
+    /**
+     * Invoked when the table needs to render a text in the "Name" column of the category table.
+     */
+    private static ObservableValue<String> getCategoryName(final TableColumn.CellDataFeatures<Category,String> cell) {
+        final InternationalString name = cell.getValue().getName();
+        return (name != null) ? new ImmutableObjectProperty<>(name.toString()) : null;
     }
 }
diff --git a/application/sis-javafx/src/main/java/org/apache/sis/internal/gui/ImmutableObjectProperty.java b/application/sis-javafx/src/main/java/org/apache/sis/internal/gui/ImmutableObjectProperty.java
index c933c5e..28faa57 100644
--- a/application/sis-javafx/src/main/java/org/apache/sis/internal/gui/ImmutableObjectProperty.java
+++ b/application/sis-javafx/src/main/java/org/apache/sis/internal/gui/ImmutableObjectProperty.java
@@ -26,7 +26,10 @@ import javafx.beans.property.ReadOnlyObjectProperty;
  *
  * @author  Martin Desruisseaux (Geomatys)
  * @version 1.1
- * @since   1.1
+ *
+ * @param  <T>  the type of value stored in the property.
+ *
+ * @since 1.1
  * @module
  */
 public class ImmutableObjectProperty<T> extends ReadOnlyObjectProperty<T> {
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
index 03cabba..4ddd450 100644
--- 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
@@ -16,92 +16,44 @@
  */
 package org.apache.sis.internal.gui.control;
 
+import javafx.collections.ObservableList;
 import javafx.event.ActionEvent;
 import javafx.event.EventHandler;
 import javafx.scene.Node;
 import javafx.scene.control.ColorPicker;
+import javafx.scene.control.ComboBox;
 import javafx.scene.control.ComboBoxBase;
 import javafx.scene.control.ContentDisplay;
+import javafx.scene.control.Control;
+import javafx.scene.control.ListCell;
 import javafx.scene.control.TableCell;
+import javafx.scene.control.TableRow;
+import javafx.scene.control.TableView;
 import javafx.scene.paint.Color;
 import javafx.scene.paint.Paint;
 import javafx.scene.shape.Rectangle;
+import org.apache.sis.internal.gui.GUIUtilities;
 
 
 /**
- * Cell representing the color of an object.
+ * Cell showing the color of an object (category, isoline, <i>etc</i>).
  * The color can be modified by selecting the table row, then clicking on the color.
- * Subclasses should override the following methods:
+ * The conversion between {@link ColorRamp} and the {@code <S>} object for row data
+ * is handled by a {@link ColorColumnHandler}. The same handler may be shared by all
+ * {@code ColorCell}s in a table. All methods are invoked in JavaFX thread.
  *
- * <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>
+ * <p>The interfaces implemented by this class are implementation convenience that may change in any future version.
+ * {@link EventHandler} is for reacting to user color selection using the control shown.</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.
+ * @param  <S>  the type of row data as declared in the {@code TableView} generic type.
  *
  * @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;
-        }
-    }
-
+final class ColorCell<S> extends TableCell<S,ColorRamp> implements EventHandler<ActionEvent> {
     /**
      * Space (in pixels) to remove on right side of the rectangle showing colors.
      */
@@ -113,89 +65,157 @@ public abstract class ColorCell<S,T extends ColorCell.Item> extends TableCell<S,
     private static final double HEIGHT = 16;
 
     /**
+     * The converter between row data and the item to store in this {@code ColorCell}.
+     * There is typically only one instance for the whole color column.
+     */
+    private final ColorColumnHandler<S> handler;
+
+    /**
+     * The type of color ramp as determined by {@link ColorColumnHandler#applyColors(Object, ColorRamp)}.
+     * This is updated by {@link #updateItem(ColorRamp, boolean)} when the value changes and stored for
+     * keeping that value stable (this class does not support mutable colors type).
+     * May be {@code null} if there is no value in the row of this cell.
+     */
+    private ColorRamp.Type type;
+
+    /**
      * 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)}.
+     * The control for selecting a color ramp, or {@code null} if not yet created.
      */
-    private Rectangle singleColor;
+    private ComboBox<ColorRamp> colorRampChooser;
 
     /**
-     * Colors to restore if user cancels an edit action.
+     * The color(s) shown in this cell when no control is shown, or {@code null} if not yet created.
+     * User can modify the color of this rectangle with {@link #colorPicker} or {@link #colorRampChooser}.
      */
-    private T restoreOnCancel;
+    private Rectangle colorView;
 
     /**
      * Creates a new cell for the colors column.
+     *
+     * @param  handler  the converter between row data and the item to store in this {@code ColorCell}.
      */
-    protected ColorCell() {
+    ColorCell(final ColorColumnHandler<S> handler) {
+        this.handler = handler;
         setContentDisplay(ContentDisplay.GRAPHIC_ONLY);
+        setOnMouseClicked((event) -> requestEdit());
     }
 
     /**
-     * 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.
+     * Returns {@code true} if neither {@link #colorPicker} or {@link #colorRampChooser} has the focus.
+     * This is used for assertions: we should not add or remove (by calls to {@link #setGraphic(Node)})
+     * one of those {@link ComboBoxBase}s before it has lost focus, otherwise it causes problems with
+     * the focus system.
      *
-     * @return initial color or paint for a new cell.
+     * @return {@code true} if no control has the focus.
      */
-    protected abstract T getDefaultItem();
+    private boolean controlNotFocused() {
+        return (colorPicker == null || !colorPicker.isFocused()) &&
+                (colorRampChooser == null || !colorRampChooser.isFocused());
+    }
 
     /**
-     * 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>
+     * Shows the control button (not the popup window) for choosing color(s) in this cell. The control type will be
+     * {@link ColorPicker} or {@link ComboBox} depending on {@link ColorColumnHandler#applyColors(Object, ColorRamp)}.
+     * This method is invoked when edition started and does nothing if the button is already visible in the cell.
      *
-     * @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}.
+     * @return the control shown, or {@code null} if none.
      *
-     * @param  colors  the initial color or gradient to show.
-     * @return the control to show to user.
+     * @see #hideControlButton()
      */
-    protected ComboBoxBase<?> getControlForEdit(final T colors) {
-        if (colorPicker == null) {
-            colorPicker = new ColorPicker();
-            addListeners(colorPicker);
+    private ComboBoxBase<?> showControlButton() {
+        final Node current = getGraphic();
+        if (current instanceof ComboBoxBase<?>) {
+            return (ComboBoxBase<?>) current;
         }
-        colorPicker.setValue(colors.color());
-        return colorPicker;
+        assert controlNotFocused();
+        final ComboBoxBase<?> control;
+        if (type == null) {
+            control = null;
+        } else {
+            final boolean isNewControl;
+            switch (type) {
+                default: throw new AssertionError(type);
+                case SOLID: {
+                    if (isNewControl = (colorPicker == null)) {
+                        colorPicker = new ColorPicker();
+                        colorPicker.setMaxWidth(Double.MAX_VALUE);      // Take all the width inside the cell.
+                        updateColorPicker(getItem());
+                    }
+                    control = colorPicker;
+                    break;
+                }
+                case GRADIENT: {
+                    if (isNewControl = (colorRampChooser == null)) {
+                        colorRampChooser = new ComboBox<>();
+                        colorRampChooser.setEditable(false);
+                        colorRampChooser.setMaxWidth(Double.MAX_VALUE);
+                        colorRampChooser.setCellFactory((column) -> new RampChoice());
+                        colorRampChooser.getItems().setAll(ColorRamp.GRAYSCALE, ColorRamp.BELL);
+                        updateColorRampChooser(getItem());
+                    }
+                    control = colorRampChooser;
+                    break;
+                }
+            }
+            /*
+             * Add listeners only after the control got its initial value, for avoiding change event.
+             * We do not need to update the value here after control creation because future updates
+             * are handled by `updateItem(…)`.
+             */
+            if (isNewControl) {
+                control.setOnAction(this);
+                control.setOnShown((event) -> requestEdit());
+                control.setOnHidden((event) -> hidden());
+            }
+        }
+        setGraphic(control);
+        return control;
     }
 
     /**
-     * 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.
+     * Cell for a color ramp in a list of choices shown by {@link ComboBox}.
+     * This is used by {@link #showControlButton()} for building {@link #colorRampChooser}.
      */
-    protected boolean controlNotFocused() {
-        return (colorPicker == null) || !colorPicker.isFocused();
+    private final class RampChoice extends ListCell<ColorRamp> {
+        /** Creates a new combo box choice. */
+        RampChoice() {
+            setContentDisplay(ContentDisplay.GRAPHIC_ONLY);
+            setMaxWidth(Double.POSITIVE_INFINITY);
+        }
+
+        /** Sets the colors to show in the combo box item. */
+        @Override protected void updateItem(final ColorRamp colors, final boolean empty) {
+            super.updateItem(colors, empty);
+            if (colors == null) {
+                setGraphic(null);
+            } else {
+                Rectangle r = (Rectangle) getGraphic();
+                if (r == null) {
+                    r = createRectangle(-40);
+                    setGraphic(r);
+                }
+                r.setFill(colors.paint());
+            }
+        }
     }
 
     /**
-     * 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)}.
+     * Creates the graphic to draw in a table cell or combo box cell for showing color(s).
+     * This method is invoked for building an editor in {@link #showControlButton()} or for
+     * rendering the {@link ColorRamp} in the table.
      *
      * @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.
+     *
+     * @see #setColorItem(ColorRamp)
      */
-    protected final Rectangle createRectangle(final double adjust) {
+    private Rectangle createRectangle(final double adjust) {
         final Rectangle gr = new Rectangle();
         gr.setHeight(HEIGHT);
         gr.widthProperty().bind(widthProperty().add(adjust));
@@ -203,30 +223,39 @@ public abstract class ColorCell<S,T extends ColorCell.Item> extends TableCell<S,
     }
 
     /**
-     * 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.
+     * Updates {@link #colorPicker} for the new item value.
      */
-    protected final void addListeners(final ComboBoxBase<?> control) {
-        control.setOnAction(this);
-        control.setOnHidden((e) -> hidden());
+    private void updateColorPicker(final ColorRamp item) {
+        colorPicker.setValue(item != null ? item.color() : null);
     }
 
-
-    //
-    // Methods below this line should not be called or overridden by subclasses.
-    //
-
+    /**
+     * Updates {@link #colorRampChooser} for the new item value. This method declares the given {@link ColorRamp}
+     * as the selected item in the chooser. If the item is not found, then it is added to the chooser list.
+     */
+    private void updateColorRampChooser(final ColorRamp item) {
+        if (item != null) {
+            final ObservableList<ColorRamp> items = colorRampChooser.getItems();
+            int i = items.indexOf(item);
+            if (i < 0) {
+                i = items.size();
+                items.add(item);
+            }
+            colorRampChooser.getSelectionModel().select(i);
+        } else {
+            colorRampChooser.getSelectionModel().clearSelection();
+        }
+    }
 
     /**
      * 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.
+     * or because this cell is now used for a new {@code <S>} instance. This method is invoked when the
+     * row value (of type {@code <S>}) is modified.
      *
      * <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.
+     * (the parent of {@link ComboBox} and {@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>
      *
@@ -234,17 +263,47 @@ public abstract class ColorCell<S,T extends ColorCell.Item> extends TableCell<S,
      * @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) {
+    protected final void updateItem(final ColorRamp colors, final boolean empty) {
         super.updateItem(colors, empty);
-        final Node control = getGraphic();
-        if (colors != null) {
-            if (control == null) {
-                setColorItem(colors);
-            } else {
-                colors.updateControl(control);
+        /*
+         * Associate the new colors to the row in a way determined by the `ColorColumnHandler` class.
+         * Then get the new color type (solid or gradient) for the current row. Note that `TableRow`
+         * may be null early in the `TableCell` lifecycle.
+         */
+        type = null;
+        if (!empty) {
+            final TableRow<S> row = getTableRow();
+            if (row != null) {
+                final S item = row.getItem();
+                if (item != null) {
+                    type = handler.applyColors(item, colors);
+                }
             }
-        } else if (control instanceof Rectangle) {
-            setGraphic(null);
+        }
+        /*
+         * Update the visual representation. Update also the control even if it is hidden, because
+         * those updates should be less frequent than show/hide cycles. It avoids the need to update
+         * the control every time that it is shown.
+         */
+        if (type != null) {
+            switch (type) {
+                default: throw new AssertionError(type);
+                case SOLID: {
+                    if (colorPicker != null) {
+                        updateColorPicker(colors);
+                    }
+                    break;
+                }
+                case GRADIENT: {
+                    if (colorRampChooser != null) {
+                        updateColorRampChooser(colors);
+                    }
+                    break;
+                }
+            }
+        }
+        if (!(getGraphic() instanceof Control)) {
+            setColorItem(colors);
         }
     }
 
@@ -255,16 +314,16 @@ public abstract class ColorCell<S,T extends ColorCell.Item> extends TableCell<S,
      *
      * @param  colors  current value of {@link #getItem()}.
      */
-    private void setColorItem(final T colors) {
+    private void setColorItem(final ColorRamp colors) {
         assert controlNotFocused();
         Rectangle view = null;
         if (colors != null) {
             final Paint paint = colors.paint();
             if (paint != null) {
-                if (singleColor == null) {
-                    singleColor = createRectangle(WIDTH_ADJUST);
+                if (colorView == null) {
+                    colorView = createRectangle(WIDTH_ADJUST);
                 }
-                view = singleColor;
+                view = colorView;
                 view.setFill(paint);
             }
         }
@@ -272,38 +331,70 @@ public abstract class ColorCell<S,T extends ColorCell.Item> extends TableCell<S,
     }
 
     /**
+     * Hides the control button for choosing color(s) in this cell. The focus is transferred to the parent table.
+     * This method does nothing if the control is already hidden.
+     *
+     * @see #showControlButton()
+     */
+    private void hideControlButton() {
+        final Node control = getGraphic();
+        if (control instanceof Control) {
+            if (control.isFocused()) {
+                // Must be before `setGraphic(…)` for causing ColorPicker to lost focus.
+                getTableView().requestFocus();
+            }
+            setColorItem(getItem());
+        }
+    }
+
+    /**
+     * Requests this cell to transition to editing state. The request is made on the {@link TableView},
+     * which will invoke {@link #startEdit()}. The edition request must be done on {@code TableView} for
+     * allowing the table to know to row and column index of the cell being edited.
+     */
+    private void requestEdit() {
+        if (isEditing()) {
+            showControlButton();
+        } else {
+            final int row = getTableRow().getIndex();
+            final TableView<S> table = getTableView();
+            table.getSelectionModel().select(row);
+            table.edit(row, getTableColumn());
+            // JavaFX will call `startEdit()`.
+        }
+    }
+
+    /**
      * 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.
+     *
+     * <p>This method should not be invoked directly. Invoke {@link #requestEdit()} instead.</p>
      */
     @Override
     public final void startEdit() {
-        restoreOnCancel = getItem();
-        final T colors = (restoreOnCancel != null) ? restoreOnCancel : getDefaultItem();
-        final ComboBoxBase<?> control = getControlForEdit(colors);
+        final ComboBoxBase<?> control = showControlButton();
         /*
          * 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();
+        if (control != null) {
+            control.requestFocus();     // Must be before `show()`, otherwise there is apparent focus confusion.
+            control.show();             // For requiring one less mouse click by user.
+        }
     }
 
     /**
      * 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.
+     * This method is automatically invoked when the user clicks on another table row.
      */
     @Override
     public final void cancelEdit() {
-        setItem(restoreOnCancel);
-        restoreOnCancel = null;
         super.cancelEdit();
-        assert controlNotFocused();
-        setColorItem(getItem());
+        hideControlButton();
     }
 
     /**
@@ -316,20 +407,24 @@ public abstract class ColorCell<S,T extends ColorCell.Item> extends TableCell<S,
      * 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.
+        if (!isHover()) {
+            if (isEditing()) {
+                getTableView().edit(-1, null);          // Cancel editing.
             }
-            setItem(restoreOnCancel);
-            super.cancelEdit();
+            hideControlButton();
         }
-        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 handler creates an item for the new color(s). The selected value may be an instance
+     * of one of the following classes:
+     *
+     * <ul>
+     *   <li>{@link Color} if the chooser was {@link ColorPicker}.</li>
+     *   <li>{@code ColorRamp} if the chooser was {@code ComboBox<ColorRamp>}.</li>
+     * </ul>
+     *
      * 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.
@@ -337,14 +432,18 @@ public abstract class ColorCell<S,T extends ColorCell.Item> extends TableCell<S,
     @Override
     public final void handle(final ActionEvent event) {
         if (isEditing()) {
+            final ColorRamp colors;
             final Object source = event.getSource();
-            final T value;
             if (source instanceof ComboBoxBase<?>) {
-                value = createItemForSelection(((ComboBoxBase<?>) source).getValue());
-            } else {
-                value = restoreOnCancel;
+                final Object value = ((ComboBoxBase<?>) source).getValue();
+                if (value instanceof Color) {
+                    colors = new ColorRamp(GUIUtilities.toARGB((Color) value));
+                } else {
+                    // A ClassCastException here would be a bug in ColorCell editors management.
+                    colors = (ColorRamp) value;
+                }
+                commitEdit(colors);
             }
-            commitEdit(value);
         }
     }
 }
diff --git a/application/sis-javafx/src/main/java/org/apache/sis/internal/gui/control/ColorColumnHandler.java b/application/sis-javafx/src/main/java/org/apache/sis/internal/gui/control/ColorColumnHandler.java
new file mode 100644
index 0000000..e89b02c
--- /dev/null
+++ b/application/sis-javafx/src/main/java/org/apache/sis/internal/gui/control/ColorColumnHandler.java
@@ -0,0 +1,124 @@
+/*
+ * 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.util.Callback;
+import javafx.scene.input.KeyCode;
+import javafx.scene.input.KeyEvent;
+import javafx.scene.control.TableView;
+import javafx.scene.control.TableColumn;
+import javafx.beans.value.ObservableValue;
+import org.apache.sis.util.resources.Vocabulary;
+import org.apache.sis.internal.gui.ImmutableObjectProperty;
+
+
+/**
+ * Builds and configures a {@link TableColumn} for colors. The {@link TableView} owner may be a table of
+ * coverage categories or a table of isolines among others. {@code ColorColumnHandler} does conversions
+ * between row data and {@link ColorRamp} items. There is typically a single {@code ColorColumnHandler}
+ * instance shared by all {@link ColorCell} in the same column of a table.
+ *
+ * <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 row data as declared in the {@link TableView} generic type.
+ *
+ * @since 1.1
+ * @module
+ */
+public abstract class ColorColumnHandler<S> implements Callback<TableColumn.CellDataFeatures<S,ColorRamp>, ObservableValue<ColorRamp>> {
+    /**
+     * Builds a new color table handler.
+     */
+    protected ColorColumnHandler() {
+    }
+
+    /**
+     * Sets the color(s) associated to the given row item and returns the color type (solid or gradient).
+     * The color type does not necessarily depend on the given {@code ColorRamp}; it may depend on the row
+     * item instead, at implementation choice. The type determines which control (color picker, combo box,
+     * <i>etc.</i>) will be shown if user wants to edit the color.
+     *
+     * @param  row      the row to update.
+     * @param  newItem  the new color(s) to assign to the given row item. May be {@code null}.
+     * @return the type of color (solid or gradient) shown for the given value.
+     */
+    protected abstract ColorRamp.Type applyColors(S row, ColorRamp newItem);
+
+    /**
+     * Gets the ARGB codes of colors to shown in the cell for the given row data.
+     *
+     * @param  row  the row item for which to get ARGB codes to show in color cell.
+     * @return the colors as ARGB codes.
+     */
+    protected abstract int[] getARGB(S row);
+
+    /**
+     * Invoked by {@link TableColumn} for computing the value of a {@link ColorCell}.
+     * This method is public as an implementation side-effect; do not rely on that.
+     *
+     * @param  cell  the row value together with references to column and table where the show the color cell.
+     * @return the color cell value.
+     */
+    @Override
+    public final ObservableValue<ColorRamp> call(final TableColumn.CellDataFeatures<S,ColorRamp> cell) {
+        final S value = cell.getValue();
+        if (value != null) {
+            final int[] ARGB = getARGB(value);
+            if (ARGB != null) {
+                return new ImmutableObjectProperty<>(new ColorRamp(ARGB));
+            }
+        }
+        return null;
+    }
+
+    /**
+     * Adds a colors column to the specified table.
+     * This method also modifies the table configuration.
+     *
+     * @param  table       the table where to add a colors column.
+     * @param  vocabulary  localized resources, provided in argument because often already known by caller.
+     */
+    protected final void addColumnTo(final TableView<S> table, final Vocabulary vocabulary) {
+        final TableColumn<S,ColorRamp> colors = new TableColumn<>(vocabulary.getString(Vocabulary.Keys.Colors));
+        colors.setCellFactory((column) -> new ColorCell<S>(this));
+        colors.setCellValueFactory(this);
+        colors.setSortable(false);
+        colors.setId("colors");
+        /*
+         * Filters are invoked during the phase when events are propagated from root to target (in contrast
+         * to handlers which are invoked in a later phase when events are propagated in opposite direction).
+         * By registering a filter, we intercept (consume) the event early and avoid that `TableCell` tries
+         * to handle it. This is necessary for avoiding `NullPointerException` observed in our experiments.
+         * That exception occurred in JavaFX code that we do not control. Note: we tried to register filter
+         * directly on the cell, but it is apparently too late for preventing the `NullPointerException`.
+         */
+        table.addEventFilter(KeyEvent.KEY_PRESSED, (event) -> {
+            if (event.getCode() == KeyCode.ENTER) {
+                event.consume();
+                final int row = table.getSelectionModel().getSelectedIndex();
+                if (row >= 0) {
+                    table.edit(row, colors);
+                }
+            }
+        });
+        table.getColumns().add(colors);
+    }
+}
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/internal/gui/control/ColorRamp.java
similarity index 55%
rename from application/sis-javafx/src/main/java/org/apache/sis/gui/coverage/CategoryColors.java
rename to application/sis-javafx/src/main/java/org/apache/sis/internal/gui/control/ColorRamp.java
index adf2e56..017016a 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/internal/gui/control/ColorRamp.java
@@ -14,11 +14,10 @@
  * See the License for the specific language governing permissions and
  * limitations under the License.
  */
-package org.apache.sis.gui.coverage;
+package org.apache.sis.internal.gui.control;
 
 import java.util.Arrays;
-import javafx.collections.ObservableList;
-import javafx.scene.Node;
+import javafx.scene.control.ColorPicker;
 import javafx.scene.control.ComboBox;
 import javafx.scene.paint.Color;
 import javafx.scene.paint.CycleMethod;
@@ -27,102 +26,107 @@ import javafx.scene.paint.Paint;
 import javafx.scene.paint.Stop;
 import org.apache.sis.internal.gui.ColorName;
 import org.apache.sis.internal.gui.GUIUtilities;
-import org.apache.sis.internal.gui.control.ColorCell;
 import org.apache.sis.util.resources.Vocabulary;
 
 
 /**
- * Represents a single color or a color ramp that can be represented in {@link CategoryColorsCell}.
+ * A single color or a gradient of colors shown as a rectangle in a {@link ColorCell}.
+ * Can also produce a string representation to be shown in a list.
+ * Instances should be considered immutable.
  *
  * @author  Martin Desruisseaux (Geomatys)
  * @version 1.1
- * @since   1.1
+ *
+ * @see ColorCell#getItem()
+ *
+ * @since 1.1
  * @module
  */
-final class CategoryColors extends ColorCell.Item {
+public final class ColorRamp {
+    /**
+     * The type of colors to shown in a cell.
+     * The type determines how user can choose a value.
+     */
+    public enum Type {
+        /**
+         * Single color selected by {@link ColorPicker}.
+         */
+        SOLID,
+
+        /**
+         * Gradient of colors (or a color ramp) selected by {@link ComboBox}.
+         */
+        GRADIENT
+    }
+
     /**
-     * Default color palette.
+     * Default color ramp.
      */
-    static final CategoryColors GRAYSCALE = new CategoryColors(0xFF000000, 0xFFFFFFFF);
+    static final ColorRamp GRAYSCALE = new ColorRamp(0xFF000000, 0xFFFFFFFF);
 
     /**
      * Blue – Cyan – White – Yellow – Red.
      */
-    static final CategoryColors BELL = new CategoryColors(0xFF0000FF, 0xFF00FFFF, 0xFFFFFFFF, 0xFFFFFF00, 0xFFFF0000);
+    static final ColorRamp BELL = new ColorRamp(0xFF0000FF, 0xFF00FFFF, 0xFFFFFFFF, 0xFFFFFF00, 0xFFFF0000);
 
     /**
      * ARGB codes of this single color or color ramp.
      * If null or empty, then default to transparent.
+     *
+     * <p><strong>This array should be read-only.</strong> We make it public because this class is internal.
+     * If this {@code ColorRamp} class moves to public API, then we would need to replace this public access
+     * by an accessor doing a copy.</p>
      */
-    final int[] colors;
+    public final int[] colors;
 
     /**
-     * The paint for this palette, created when first needed.
+     * A single color created from {@link #colors} when first needed.
+     *
+     * @see #color()
+     */
+    private transient Color color;
+
+    /**
+     * A gradient of colors created from {@link #colors} when first needed.
+     *
+     * @see #paint()
      */
     private transient Paint paint;
 
     /**
-     * A name for this palette, computed when first needed.
+     * A name for this color ramp created from {@link #colors} when first needed.
      *
      * @see #toString()
      */
     private transient String name;
 
     /**
-     * Creates a new palette for the given colors.
+     * Creates a new item for the given colors.
      */
-    CategoryColors(final int... colors) {
+    ColorRamp(final int... colors) {
         this.colors = colors;
     }
 
     /**
-     * Updates a control with the current color of this view.
+     * Returns a solid color to use for filling a rectangle in {@link ColorCell}.
+     * If this item has many colors (for example because it uses a gradient),
+     * then an arbitrary color is returned.
      *
-     * @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.
+     * @return single color to shown in table cell, or {@code null} if none.
      */
-    final void asSelectedItem(final ComboBox<CategoryColors> colorRampChooser) {
-        final ObservableList<CategoryColors> items = colorRampChooser.getItems();
-        int i = items.indexOf(this);
-        if (i < 0) {
-            i = items.size();
-            items.add(this);
+    final Color color() {
+        if (color == null && colors != null && colors.length != 0) {
+            color = GUIUtilities.fromARGB(colors[colors.length / 2]);
         }
-        colorRampChooser.getSelectionModel().select(i);
+        return color;
     }
 
     /**
-     * Returns the first color, or {@code null} if none.
-     * This is used for qualitative categories, which are expected to contain only one color.
-     */
-    @Override
-    protected final Color color() {
-        if (colors != null && colors.length != 0) {
-            return GUIUtilities.fromARGB(colors[0]);
-        } else {
-            return null;
-        }
-    }
-
-    /**
-     * Gets the paint to use for filling a rectangle using this color palette.
-     * Returns {@code null} if this {@code CategoryColors} contains no color.
+     * Returns the paint to use for filling a rectangle in {@link ColorCell}, or {@code null} if none.
+     *
+     * @return color or gradient paint for table cell, or {@code null} if none.
      */
-    @Override
-    protected final Paint paint() {
+    final Paint paint() {
         if (paint == null) {
             switch (colors.length) {
                 case 0: break;
@@ -137,6 +141,7 @@ final class CategoryColors extends ColorCell.Item {
                         stops[i] = new Stop(scale*i, GUIUtilities.fromARGB(colors[i]));
                     }
                     paint = new LinearGradient(0, 0, 1, 0, true, CycleMethod.NO_CYCLE, stops);
+                    break;
                 }
             }
         }
@@ -144,8 +149,8 @@ final class CategoryColors extends ColorCell.Item {
     }
 
     /**
-     * Returns a string representation of this color palette.
-     * This string representation will appear in the combo box when that box is shown.
+     * Returns a string representation of this color ramp.
+     * It may be used as an alternative to colored rectangle.
      */
     @Override
     public String toString() {
@@ -170,8 +175,10 @@ final class CategoryColors extends ColorCell.Item {
     }
 
     /**
-     * Returns a hash code value for this palette.
+     * Returns a hash code value for this color ramp.
      * Defined mostly for consistency with {@link #equals(Object)}.
+     *
+     * @return a hash code value for this color ramp.
      */
     @Override
     public int hashCode() {
@@ -179,10 +186,14 @@ final class CategoryColors extends ColorCell.Item {
     }
 
     /**
-     * Returns whether the given object is equal to this {@code CategoryColors}.
+     * Returns whether the given object is equal to this {@code ColorRamp}.
+     * This is used for locating this {@code ColorRamp} in a {@link ComboBox}.
+     *
+     * @param  other  the object to compare with {@code this} for equality.
+     * @return whether the given object is equal to this color ramp.
      */
     @Override
     public boolean equals(final Object other) {
-        return (other instanceof CategoryColors) && Arrays.equals(colors, ((CategoryColors) other).colors);
+        return (other instanceof ColorRamp) && Arrays.equals(colors, ((ColorRamp) other).colors);
     }
 }
diff --git a/application/sis-javafx/src/test/java/org/apache/sis/gui/coverage/CoverageStylingApp.java b/application/sis-javafx/src/test/java/org/apache/sis/gui/coverage/CoverageStylingApp.java
index a66da1d..815d5d8 100644
--- a/application/sis-javafx/src/test/java/org/apache/sis/gui/coverage/CoverageStylingApp.java
+++ b/application/sis-javafx/src/test/java/org/apache/sis/gui/coverage/CoverageStylingApp.java
@@ -20,6 +20,7 @@ import java.util.Locale;
 import javafx.stage.Stage;
 import javafx.application.Application;
 import javafx.scene.Scene;
+import javafx.scene.control.Button;
 import javafx.scene.control.TableView;
 import javafx.scene.layout.BorderPane;
 import org.apache.sis.coverage.Category;
@@ -55,7 +56,8 @@ public final strictfp class CoverageStylingApp extends Application {
     public void start(final Stage window) {
         final BorderPane pane = new BorderPane();
         pane.setCenter(createCategoryTable());
-        window.setTitle("BandColorsTable Test");
+        pane.setBottom(new Button("Focus here"));
+        window.setTitle("CategoryTable Test");
         window.setScene(new Scene(pane));
         window.setWidth (400);
         window.setHeight(300);
@@ -76,7 +78,7 @@ public final strictfp class CoverageStylingApp extends Application {
 
         final CoverageStyling styling = new CoverageStyling(null);
         styling.setARGB(band.getCategories().get(1), new int[] {0xFF607080});
-        final TableView<Category> table = CategoryColorsCell.createTable(styling, Vocabulary.getResources((Locale) null));
+        final TableView<Category> table = styling.createCategoryTable(Vocabulary.getResources((Locale) null));
         table.getItems().setAll(band.getCategories());
         return table;
     }


Mime
View raw message