/*
* Geotoolkit.org - An Open Source Java GIS Toolkit
* http://www.geotoolkit.org
*
* (C) 2010-2012, Open Source Geospatial Foundation (OSGeo)
* (C) 2010-2012, Geomatys
*
* This library is free software; you can redistribute it and/or
* modify it under the terms of the GNU Lesser General Public
* License as published by the Free Software Foundation;
* version 2.1 of the License.
*
* This library is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
* Lesser General Public License for more details.
*/
package org.geotoolkit.gui.swing.image;
import java.util.Set;
import java.util.List;
import java.util.Locale;
import java.util.Arrays;
import java.util.ArrayList;
import java.util.Collections;
import java.util.EventObject;
import java.util.concurrent.Callable;
import java.awt.Color;
import java.awt.Component;
import java.awt.BorderLayout;
import java.awt.event.MouseEvent;
import javax.swing.JTable;
import javax.swing.JComboBox;
import javax.swing.JComponent;
import javax.swing.ComboBoxModel;
import javax.swing.AbstractCellEditor;
import javax.swing.table.TableCellEditor;
import javax.swing.table.TableCellRenderer;
import javax.swing.table.TableColumn;
import org.geotoolkit.image.color.ColorUtilities;
import org.geotoolkit.resources.Vocabulary;
import org.geotoolkit.image.palette.Palette;
import org.geotoolkit.image.palette.PaletteFactory;
import org.geotoolkit.image.io.SpatialImageReadParam;
import org.geotoolkit.internal.coverage.ColorPalette;
/**
* A combo box for selecting a color {@linkplain Palette palette}. The choices of available
* palettes is provided by a {@linkplain PaletteFactory palette factory} given to the constructor.
* <p>
* This combo box can also be used as a cell editor in a {@link JTable},
* by invoking the {@link #useAsTableCellEditor(TableColumn)} method.
*
* @author Martin Desruisseaux (Geomatys)
* @version 3.17
*
* @since 3.13
* @module
*/
@SuppressWarnings("serial")
public class PaletteComboBox extends JComponent {
/**
* The factory used for loading colors from a palette name.
*/
private final PaletteFactory factory;
/**
* The combo box providing color palette choices.
*/
private final JComboBox<Object> comboBox;
/**
* Creates a new combo box using the
* {@linkplain PaletteFactory#getDefault() default palette factory}.
*/
public PaletteComboBox() {
this(null);
}
/**
* Creates a new combo box using the given palette factory. The combo box content will be
* initialized to the {@linkplain PaletteFactory#getAvailableNames() available names}.
*
* @param factory The factory to use for loading colors from a palette name, or
* {@code null} for the {@linkplain PaletteFactory#getDefault() default}.
*/
public PaletteComboBox(PaletteFactory factory) {
if (factory == null) {
factory = PaletteFactory.getDefault();
}
this.factory = factory;
final Locale locale = getLocale();
final Vocabulary resources = Vocabulary.getResources(locale);
Set<String> names = factory.getAvailableNames();
if (names == null) {
/*
* Color palettes can not be found (note that this is not the same than an empty set,
* which means "no color palette"). For now, assume that we have only the "grayscale"
* palette. This palette exists in the default Geotk implementation. Even if it does
* not exist, the current ColorRampRenderer behavior if the color palette is not found
* is to fallback on a grayscale palette.
*/
names = Collections.singleton(SpatialImageReadParam.DEFAULT_PALETTE_NAME);
}
final List<Object> items = new ArrayList<>(names.size() + 1);
items.add(resources.getString(Vocabulary.Keys.None));
for (final String name : names) {
items.add(new ColorPalette(name));
}
comboBox = new JComboBox<>(items.toArray());
comboBox.setPrototypeDisplayValue(SpatialImageReadParam.DEFAULT_PALETTE_NAME); // For preventing pre-rendering of all palettes.
comboBox.setRenderer(new PaletteCellRenderer(comboBox.getModel(), factory, locale));
setLayout(new BorderLayout());
add(comboBox, BorderLayout.CENTER);
}
/**
* Adds a uniform color (typically opaque) to the list of proposed choices.
* The color will be inserted before the gradients.
*
* @param color The uniform color to add.
*/
public void addColor(final Color color) {
final int size = comboBox.getItemCount();
// The element at index 0 is "none", so we need to skip it.
for (int i=1; i<size; i++) {
if (!(comboBox.getItemAt(i) instanceof Color)) {
comboBox.insertItemAt(color, i);
return;
}
}
comboBox.addItem(color);
}
/**
* Adds a default set of colors to the list of proposed choices.
* The colors added are the constants declared in the {@link Color} class.
*/
public void addDefaultColors() {
addColor(Color.WHITE);
addColor(Color.LIGHT_GRAY);
addColor(Color.GRAY);
addColor(Color.DARK_GRAY);
addColor(Color.BLACK);
addColor(Color.YELLOW);
addColor(Color.ORANGE);
addColor(Color.PINK);
addColor(Color.MAGENTA);
addColor(Color.RED);
addColor(Color.GREEN);
addColor(Color.CYAN);
addColor(Color.BLUE);
}
/**
* Returns the name of the currently selected item, or {@code null} if none.
* <p>
* <ul>
* <li>If the selected item is a color palette, then this method returns the
* name of that palette. This is typically one of the values listed in the
* {@link PaletteFactory} javadoc.</li>
* <li>If the selected item is a uniform color, then this method returns the
* {@code '#'} character followed by the hexadecimal code of that color.</li>
* <li>Otherwise this method returns {@code null}.</li>
* </ul>
*
* @return The name of the currently selected item, or {@code null} if none.
*/
public String getSelectedItem() {
final Object item = comboBox.getSelectedItem();
if (item instanceof ColorPalette) {
return ((ColorPalette) item).paletteName;
} else if (item instanceof Color) {
return ColorUtilities.toString((Color) item);
}
return null;
}
/**
* Sets the currently selected item by its color code or palette name.
* <p>
* <ul>
* <li>If the given name is {@code null}, then the "<cite>none</cite>" choice is
* selected.</li>
* <li>Otherwise if the given name starts with the {@code '#'} character, then the
* name is decoded as a color using the {@link Color#decode(String)} method
* and the corresponding color is selected.</li>
* <li>Otherwise the given name is used as the name of a color palette, and that
* palette is selected.</li>
* </ul>
* <p>
* If no color or palette is found for the given name, then this method selects
* the "<cite>none</cite>" choice (same as if the name is {@code null}).
*
* @param name The color code or palette name to select, or {@code null} if none.
*/
public void setSelectedItem(String name) {
int index = 0; // Index of the "none" choice.
if (name != null) {
index = comboBox.getItemCount();
Object toSearch = name = name.trim();
if (name.startsWith("#")) try {
toSearch = Color.decode(name);
} catch (NumberFormatException e) {
// Ignore: we will search for the name as a string.
}
while (--index != 0) {
Object candidate = comboBox.getItemAt(index);
if (candidate instanceof ColorPalette) {
candidate = ((ColorPalette) candidate).paletteName;
}
if (toSearch.equals(candidate)) {
break;
}
}
}
comboBox.setSelectedIndex(index);
}
/**
* Returns the colors for the currently selected item, or {@code null} if none.
*
* @return The colors of the currently selected item, or {@code null} if none.
*
* @since 3.14
*/
public Color[] getSelectedColors() {
final Object item = comboBox.getSelectedItem();
if (item instanceof ColorPalette) {
return ((ColorPalette) item).getColors(factory);
} else if (item instanceof Color) {
return new Color[] {(Color) item};
}
return null;
}
/**
* Sets the currently selected item by its color. This method searches for a choices
* providing the same colors than the given array. If such choices is found, it is
* selected. Otherwise this method selects the "<cite>none</cite>" choice.
*
* @param colors The colors to select, or {@code null} if none.
*
* @since 3.14
*/
public void setSelectedColors(final Color... colors) {
int index = 0; // Index of the "none" choice.
if (colors != null) {
final Color singleton = (colors.length == 1) ? colors[0] : null;
index = comboBox.getItemCount();
while (--index != 0) {
final Object candidate = comboBox.getItemAt(index);
if (singleton != null && singleton.equals(candidate)) {
break;
}
if (candidate instanceof ColorPalette) {
final ColorPalette cp = (ColorPalette) candidate;
if (Arrays.equals(colors, cp.getColors(factory))) {
break;
}
}
}
}
comboBox.setSelectedIndex(index);
}
/**
* Uses this {@code PaletteComboBox} as the {@linkplain TableCellEditor table cell editor}
* and the {@linkplain TableCellRenderer table cell renderer} of the given table column.
* <p>
* This {@code PaletteComboBox} instance should not be used for any other purpose after
* this method call.
*
* @param column The table column on which to set this combo box as the editor and renderer.
*
* @since 3.14
*/
public void useAsTableCellEditor(final TableColumn column) {
// See javax.swing.DefaultCellEditor(JComboBox) source code.
comboBox.putClientProperty("JComboBox.isTableCellEditor", Boolean.TRUE);
column.setCellEditor(new CellEditor());
column.setCellRenderer((TableCellRenderer) comboBox.getRenderer());
}
/**
* A cell editor for using {@link PaletteComboBox} in a {@linkplain javax.swing.JTable}.
*
* @author Martin Desruisseaux (Geomatys)
* @version 3.14
*
* @since 3.14
* @module
*/
@SuppressWarnings("serial")
private final class CellEditor extends AbstractCellEditor implements TableCellEditor, Callable<ComboBoxModel<Object>> {
/**
* Returns {@code true} if the cell can be edited. We require a double click,
* otherwise the combo box drop down list appears and disappears too often.
*/
@Override
public boolean isCellEditable(final EventObject event) {
return !(event instanceof MouseEvent) || ((MouseEvent) event).getClickCount() >= 2;
}
/**
* Gets the name of the selected palette, or the opaque color code.
*/
@Override
public String getCellEditorValue() {
return PaletteComboBox.this.getSelectedItem();
}
/**
* Configures the {@link PaletteComboBox} to the given value, and returns it.
*/
@Override
public Component getTableCellEditorComponent(final JTable table, Object value,
final boolean isSelected, final int row, final int column)
{
PaletteComboBox.this.setSelectedItem((String) value);
return PaletteComboBox.this;
}
/**
* A trick for allowing {@link org.geotoolkit.gui.swing.coverage.CategoryTable}
* to get the list of available palettes. Should not be invoked by client code.
*/
@Override
public ComboBoxModel<Object> call() {
return comboBox.getModel();
}
}
}