/* * Geotoolkit.org - An Open Source Java GIS Toolkit * http://www.geotoolkit.org * * (C) 2003-2012, Open Source Geospatial Foundation (OSGeo) * (C) 2009-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.Locale; import java.util.ResourceBundle; import java.text.ParseException; import javax.swing.JLabel; import javax.swing.JPanel; import javax.swing.JTable; import javax.swing.JComponent; import javax.swing.JTabbedPane; import javax.swing.JScrollPane; import javax.swing.table.AbstractTableModel; import java.beans.PropertyChangeListener; import java.beans.PropertyChangeEvent; import java.awt.GridBagConstraints; import java.awt.GridBagLayout; import java.awt.BorderLayout; import java.awt.Component; import java.awt.EventQueue; import java.awt.Dimension; import java.lang.reflect.Array; import java.awt.Image; import java.awt.Insets; import java.awt.color.ColorSpace; import java.awt.image.ColorModel; import java.awt.image.DataBuffer; import java.awt.image.SampleModel; import java.awt.image.RenderedImage; import java.awt.image.IndexColorModel; import java.awt.image.renderable.RenderableImage; import javax.imageio.spi.ImageReaderWriterSpi; import javax.media.jai.IHSColorSpace; import javax.media.jai.OperationNode; import javax.media.jai.PropertySource; import javax.media.jai.PropertyChangeEmitter; import javax.media.jai.RegistryElementDescriptor; import javax.media.jai.OperationDescriptor; import org.jdesktop.swingx.JXTitledSeparator; import org.opengis.referencing.crs.CoordinateReferenceSystem; import org.geotoolkit.gui.swing.Dialog; import org.geotoolkit.internal.swing.SwingUtilities; import org.apache.sis.util.CharSequences; import org.apache.sis.measure.NumberRange; import org.apache.sis.util.Classes; import org.apache.sis.measure.RangeFormat; import org.geotoolkit.resources.Vocabulary; import org.geotoolkit.lang.Debug; import static java.awt.GridBagConstraints.*; /** * A panel showing the properties of an image. The panel contains the following tabs * (some of them may be disabled depending on the image type): * <p> * <ul> * <li>A summary with informations about the {@linkplain ColorModel color model}, * {@linkplain SampleModel sample model}, image size, tile size, <i>etc.</i></li> * <li>A table of (<var>key</var>, <var>value</var>) pairs which are the * {@linkplain RenderedImage#getPropertyNames() properties} associated with the image. * The properties include for example the minimal and maximal pixel values computed by * JAI.</li> * <li>The numerical value of each band in a table, as provided by {@link ImageSampleValues}.</li> * <li>An overview of the image, as provided by {@link ImagePane}.</li> * </ul> * <p> * While this pane works primarily with instances of the {@link RenderedImage} interface, it * accepts also instances of {@link RenderableImage} or {@link PropertySource} interfaces. * The {@link PropertySource#getProperty(String)} method will be invoked only when a property * is first required, in order to avoid the computation of deferred properties before * needed. If the source implements also the {@link PropertyChangeEmitter} interface, * then this widget will register a listener for property changes. The changes can be * emitted from any thread - it doesn't need to be the <cite>Swing</cite> thread. * * @author Martin Desruisseaux (IRD, Geomatys) * @version 3.12 * * @see ImageFileProperties * @see OperationTreeBrowser * * @since 2.3 * @module */ @SuppressWarnings("serial") public class ImageProperties extends JComponent implements Dialog { /** * Index in the {@link #descriptions} array for the text area of the operation name, * version and description. If the image is not an instance of {@link OperationNode}, * then the class name is used. If this panel is an instance of {@link ImageFileProperties}, * then this will rather be the file name. */ private static final int DESCRIPTION = 0; /** * Index in the {@link #descriptions} array for the text area of an image property. */ private static final int IMAGE_SIZE=1, TILE_SIZE=2, DATA_TYPE=3, SAMPLE_MODEL=4, COLOR_MODEL=5, COLOR_SPACE=6, COLOR_RAMP=7, VALUE_RANGE=8, CRS_NAME=9, PIXEL_SIZE=10; /** * The last {@link #descriptions} index plus one. This is the maximal array length. */ private static final int LAST = 11, LAST_NO_METADATA = VALUE_RANGE; /** * The first item which may not be present. The labels for all items * starting at this index can be enabled or disabled. */ private static final int FIRST_OPTIONAL = COLOR_RAMP; /** * An array of length {@link #LAST} (at most) of text areas for various image properties. */ private final JLabel[] descriptions; /** * The label of optional descriptions. Those label may be enabled or disabled. */ private final JLabel[] labelOptionals; /** * The color bar for {@link IndexColorModel}. */ private final ColorRamp colorRamp; /** * The table model for image properties, or {@code null} if none. * This field is {@code null} - together with {@link #samples} - when this object * is actually an instance of the {@link ImageFileProperties} subclass, because the * properties tab is replaced by image metadata. */ private final Table properties; /** * The table for sample values, or {@code null} if none. * This field is {@code null} - together with {@link #properties} - when this object * is actually an instance of the {@link ImageFileProperties} subclass, because the * later will try to load only a small portion of the image. Since the subsampling * is arbitrary, so is the table size - so we are better to not show it. */ private final ImageSampleValues samples; /** * The viewer for an image quick look. */ protected final ImagePane viewer; /** * The panel which contains the tabs. */ final JTabbedPane tabs; /** * The range format. Will be created when first needed. */ private transient RangeFormat rangeFormat; /** * Creates a new instance of {@code ImageProperties} with no image. * One of {@link #setImage(RenderedImage) setImage(...)} methods must * be invoked in order to set the properties source. */ public ImageProperties() { this((JComponent) null); } /** * Creates a new instance with the given panel as an additional "metadata" tab. * This is used for the {@link ImageFileProperties} constructor only. */ ImageProperties(final JComponent metadata) { setLayout(new BorderLayout()); final Vocabulary resources = Vocabulary.getResources(getLocale()); tabs = new JTabbedPane(); colorRamp = new ColorRamp(); descriptions = new JLabel[(metadata != null) ? LAST : LAST_NO_METADATA]; labelOptionals = new JLabel[descriptions.length - FIRST_OPTIONAL]; /* * Build the informations tab. We use a two-columns layout, with the labels on * the left side and the values on the right side. We use a loop because all * rows are processed in the same way, with a few exceptions for example in order * to add a separator before CRS informations. */ final JPanel info = new JPanel(new GridBagLayout()); final GridBagConstraints c = new GridBagConstraints(); final Insets insets = c.insets; c.gridy=0; c.anchor=WEST; c.fill=HORIZONTAL; for (int i=0; i<descriptions.length; i++) { c.gridx=0; final short labelKey; switch (i) { case DESCRIPTION: { c.insets.left=9; c.weightx=1; // No need to reset those particular settings. c.gridwidth=2; insets.bottom=15; info.add(descriptions[i] = new JLabel(" "), c); c.gridwidth=1; insets.bottom= 0; // Need to be reset for the next loop execution. continue; // Do not add label. } case IMAGE_SIZE: labelKey = Vocabulary.Keys.ImageSize; break; case TILE_SIZE: labelKey = Vocabulary.Keys.TilesSize; break; case DATA_TYPE: labelKey = Vocabulary.Keys.DataType; break; case SAMPLE_MODEL: labelKey = Vocabulary.Keys.SampleModel; break; case COLOR_MODEL: labelKey = Vocabulary.Keys.ColorModel; break; case COLOR_SPACE: labelKey = Vocabulary.Keys.ColorSpace; break; case COLOR_RAMP: labelKey = Vocabulary.Keys.Colors; break; case VALUE_RANGE: labelKey = Vocabulary.Keys.ValueRange; break; case PIXEL_SIZE: labelKey = Vocabulary.Keys.PixelSize; break; case CRS_NAME: { // Add a separator using HTML style instead than setting // the fonts in order to a have consistent looks. final JXTitledSeparator title = new JXTitledSeparator("<html><h3>" + resources.getString(Vocabulary.Keys.CoordinateReferenceSystem) + "</h3></html>"); c.gridy++; insets.left=9; // No need to reset this particular setting. c.gridwidth=2; info.add(title, c); c.gridwidth=1; // Need to be reset before to add the label. labelKey = Vocabulary.Keys.Description; break; } case LAST: // Just for a compile-time check of wrong values. default: throw new AssertionError(i); } final JLabel label = new JLabel(resources.getLabel(labelKey)); if (i >= FIRST_OPTIONAL) { labelOptionals[i - FIRST_OPTIONAL] = label; } /* * At this point we are ready to add the (label, value) pair. * We will make a special case for the color ramp, where the * value is not a label. */ c.gridy++; c.weightx=0; insets.left=40; info.add(label, c); c.gridx=1; c.weightx=1; insets.left= 9; final JComponent description; switch (i) { case COLOR_RAMP: { c.anchor=CENTER; insets.right=6; description = colorRamp; description.setOpaque(false); break; } default: { description = descriptions[i] = new JLabel(); break; } } info.setOpaque(false); info.add(description, c); label.setLabelFor(description); c.anchor=WEST; insets.right=0; } tabs.addTab(resources.getString(Vocabulary.Keys.Informations), info); /* * Build the image's properties tab and the image sample values tab. * In the particular case of ImageFileProperties, those two tabs are * replaced by a metadata tab. We do not show the sample values tab * for image file because the ImageFileProperties widget will try to * load only a small portion of image data. */ if (metadata == null) { properties = new Table(resources); final JTable table = new JTable(properties); table.setAutoCreateRowSorter(true); tabs.addTab(resources.getString(Vocabulary.Keys.Properties), new JScrollPane(table)); samples = new ImageSampleValues(); tabs.addTab(resources.getString(Vocabulary.Keys.Pixels), samples); } else { metadata.setOpaque(false); properties = null; samples = null; tabs.addTab(resources.getString(Vocabulary.Keys.Metadata), metadata); } /* * Build the image preview tab. */ viewer = new ImagePane(); viewer.setPaintingWhileAdjusting(true); tabs.addTab(resources.getString(Vocabulary.Keys.Preview), viewer.createScrollPane()); add(tabs, BorderLayout.CENTER); setPreferredSize(new Dimension(600, 400)); } /** * Create a new instance of {@code ImageProperties} for the specified * rendered image. * * @param image The image, or {@code null} if none. */ public ImageProperties(final RenderedImage image) { this(); if (image != null) { setImage(image); } } /** * Sets the operation name, description and version for the given image. If the image is * an instance of {@link OperationNode}, then a description of the operation will be fetch * from its resources bundle. * <p> * This method accepts also instances of {@link ImageReaderWriterSpi}, * for the specific needs of {@link ImageFileProperties} only. * * @param image The image, or {@code null} if none. */ final void setOperationDescription(final Object image) { final Locale locale = getLocale(); final Vocabulary resources = Vocabulary.getResources(locale); String name = resources.getString(Vocabulary.Keys.Undefined); String version = null; String description = null; String extra = null; if (image instanceof OperationNode) { /* * JAI operation - get the information from the descriptor. * We put the version number just below the operation name, * before the description, since the version applies to the * operation. */ final String mode; final RegistryElementDescriptor descriptor; final OperationNode operation = (OperationNode) image; name = operation.getOperationName(); mode = operation.getRegistryModeName(); descriptor = operation.getRegistry().getDescriptor(mode, name); if (descriptor instanceof OperationDescriptor) { final ResourceBundle bundle; bundle = ((OperationDescriptor) descriptor).getResourceBundle(locale); name = bundle .getString("LocalName"); description = bundle .getString("Description"); version = resources.getString(Vocabulary.Keys.Version_1, bundle .getString("Version")) + " (" + bundle .getString("Vendor") + ')'; name = resources.getString(Vocabulary.Keys.Operation_1, name); } } else if (image instanceof ImageReaderWriterSpi) { /* * Image Reader or Writer provider - for ImageFileProperties only. * We put the version number after the description, since the description * is actually the decodeur implementation. The "name" is the MIME type, * which doesn't have a version number. */ final ImageReaderWriterSpi spi = (ImageReaderWriterSpi) image; description = spi.getDescription(locale); extra = resources.getString(Vocabulary.Keys.Version_1, spi.getVersion()) + " (" + spi.getVendorName() + ')'; String[] names = spi.getMIMETypes(); if (names != null && names.length != 0) { name = names[0]; } else { names = spi.getFormatNames(); if (names != null && names.length != 0) { name = names[0]; } } } else if (image != null) { /* * Unknown case - typically a BufferedImage. */ name = Classes.getShortClassName(image); name = resources.getString(Vocabulary.Keys.ImageClass_1, name); } /* * Formats the description field using the information fetched above. */ final StringBuilder html = new StringBuilder("<html>"); html.append("<h2>").append(name).append("</h2>"); if (version != null) { html.append("<p>").append(version).append("</p>"); } if (description != null) { html.append("<p><cite>").append(description).append("</cite></p>"); } if (extra != null) { html.append("<p>").append(extra).append("</p>"); } descriptions[DESCRIPTION].setText(html.append("</html>").toString()); } /** * Sets all text fields to {@code null}. This method do not set the {@link #properties} * table; this is left to the caller. */ void clear() { for (int i=0; i<descriptions.length; i++) { final JLabel description = descriptions[i]; if (description != null) { description.setText(null); } } for (int i=0; i<labelOptionals.length; i++) { labelOptionals[i].setEnabled(false); } colorRamp.setColors((IndexColorModel) null); } /** * Sets the {@linkplain PropertySource property source} for this widget. If the source is a * {@linkplain RenderedImage rendered} or a {@linkplain RenderableImage renderable} image, * then the widget will be set as if the most specific flavor of {@code setImage(...)} * was invoked. * * @param image The image, or {@code null} if none. */ public void setImage(final PropertySource image) { if (image instanceof RenderedImage) { setImage((RenderedImage) image); return; } if (image instanceof RenderableImage) { setImage((RenderableImage) image); return; } clear(); setOperationDescription(image); if (properties != null) { properties.setSource(image); samples .setImage((RenderedImage) null); } viewer.setImage((RenderedImage) null); } /** * Sets the specified {@linkplain RenderableImage renderable image} as the properties source. * * @param image The image, or {@code null} if none. */ public void setImage(final RenderableImage image) { clear(); if (image != null) { final Vocabulary resources = Vocabulary.getResources(getLocale()); descriptions[IMAGE_SIZE].setText(resources.getString( Vocabulary.Keys.Size_2, image.getWidth(), image.getHeight())); } setOperationDescription(image); if (properties != null) { properties.setSource(image); samples .setImage ((RenderedImage) null); } viewer.setImage(image); } /** * Sets the specified {@linkplain RenderedImage rendered image} as the properties source. * * @param image The image, or {@code null} if none. */ public void setImage(final RenderedImage image) { if (image == null) { clear(); } else { setImageDescription(image.getColorModel(), image.getSampleModel(), image.getWidth(), image.getHeight(), image.getTileWidth(), image.getTileHeight(), image.getNumXTiles(), image.getNumYTiles()); } setOperationDescription(image); if (properties != null) { properties.setSource(image); samples .setImage (image); } viewer.setImage(image); } /** * Sets the content of the description panel, not including the part which * is specific to the image operations and the part which depends on metadata. */ final void setImageDescription(final ColorModel cm, final SampleModel sm, final int width, final int height, final int tileWidth, final int tileHeight, final int numXTiles, final int numYTiles) { final Vocabulary resources = Vocabulary.getResources(getLocale()); final IndexColorModel icm = (cm instanceof IndexColorModel) ? (IndexColorModel) cm : null; for (int i=IMAGE_SIZE; i<COLOR_RAMP; i++) { final String text; switch (i) { case IMAGE_SIZE: { final Object numBands = (sm != null) ? sm.getNumBands() : resources.getString(Vocabulary.Keys.Undefined); text = resources.getString(Vocabulary.Keys.ImageSize_3, width, height, numBands); break; } case TILE_SIZE: { text = resources.getString(Vocabulary.Keys.TileSize_4, numXTiles, numYTiles, tileWidth, tileHeight); break; } case DATA_TYPE: { text = getDataType(sm != null ? sm.getDataType() : DataBuffer.TYPE_UNDEFINED, cm, resources); break; } case SAMPLE_MODEL: { text = formatClassName(sm, resources); break; } case COLOR_MODEL: { text = formatClassName(cm, resources); break; } case COLOR_SPACE: { text = getColorSpace(cm, resources); break; } default: throw new AssertionError(i); } descriptions[i].setText(text); } colorRamp.setColors(icm); labelOptionals[COLOR_RAMP - FIRST_OPTIONAL].setEnabled(icm != null); } /** * Sets the content of the geospatial description panel. This is the same panel than the * one modified by the previous {@code setImageDecription(...)} method, but this time for * the geospatial information part. Those informations are typically extracted from the * image metadata. * * @param crs The coordinate reference system, or {@code null}. * @param cellSize The cell size as a string, or {@code null}. * @param values The range of geophysics values, or {@code null} if none. * * @since 3.08 */ @SuppressWarnings("fallthrough") final void setGeospatialDescription(final CoordinateReferenceSystem crs, final String cellSize, final NumberRange<?> values) { for (int i=VALUE_RANGE; i<LAST; i++) { String text = null; switch (i) { case VALUE_RANGE: { if (values != null) { if (rangeFormat == null) { rangeFormat = new RangeFormat(getLocale()); } text = rangeFormat.format(values); } break; } case CRS_NAME: { if (crs != null) { text = crs.getName().getCode(); } break; } case PIXEL_SIZE: { text = cellSize; break; } } descriptions[i].setText(text); labelOptionals[i - FIRST_OPTIONAL].setEnabled(true); } } /** * Sets whatever the geospatial descriptions are enabled or not. * Note that this is automatically set to {@code true} by the above method. */ final void setGeospatialDescription(final boolean enabled) { for (int i=VALUE_RANGE; i<LAST; i++) { labelOptionals[i - FIRST_OPTIONAL].setEnabled(enabled); } } /** * Returns a string representation for the given data type. * * @param type The data type (one of {@link DataBuffer} constants). * @param cm The color model for computing the pixel size in bits, or {@code null}. * @param resources The resources to use for formatting the type. * @return The data type as a localized string. */ @SuppressWarnings("fallthrough") private static String getDataType(final int type, final ColorModel cm, final Vocabulary resources) { final short key; switch (type) { case DataBuffer.TYPE_BYTE: // Fall through case DataBuffer.TYPE_USHORT: key = Vocabulary.Keys.UnsignedInteger_2; break; case DataBuffer.TYPE_SHORT: // Fall through case DataBuffer.TYPE_INT: key = Vocabulary.Keys.SignedInteger_1; break; case DataBuffer.TYPE_FLOAT: // Fall through case DataBuffer.TYPE_DOUBLE: key = Vocabulary.Keys.RealNumber_1; break; case DataBuffer.TYPE_UNDEFINED: // Fall through default: return resources.getString(Vocabulary.Keys.Undefined); } final Integer typeSize = DataBuffer.getDataTypeSize(type); final Integer pixelSize = (cm != null) ? cm.getPixelSize() : typeSize; return resources.getString(key, typeSize, pixelSize); } /** * Returns the name of the color space for the given color model. * * @param cm The color model, or {@code null} if undefined. * @param resources The resources to use for formatting the type. * @return The name of the color space. */ private static String getColorSpace(final ColorModel cm, final Vocabulary resources) { if (cm != null) { final ColorSpace cs = cm.getColorSpace(); if (cs != null) { final String text; switch (cs.getType()) { case ColorSpace.TYPE_GRAY: { text = resources.getString(Vocabulary.Keys.GrayScale); break; } case ColorSpace.TYPE_RGB: text = "RGB"; break; case ColorSpace.TYPE_CMYK: text = "CMYK"; break; case ColorSpace.TYPE_HLS: text = "HLS"; break; case ColorSpace.TYPE_HSV: { text = (cs instanceof IHSColorSpace) ? "IHS" : "HSV"; break; } default: { text = resources.getString(Vocabulary.Keys.Unknown); break; } } return text + " (" + resources.getString( Vocabulary.Keys.ComponentCount_1, cs.getNumComponents()) + ')'; } } return resources.getString(Vocabulary.Keys.Undefined); } /** * Split a class name into a more human readable sentence * (e.g. "PixelInterleavedSampleModel" into "Pixel interleaved sample model"). * * @param object The object to format, or {@code null} if undefined. * @param resources The resources to use for formatting localized text. * @return The object class name. */ private static String formatClassName(final Object object, final Vocabulary resources) { if (object == null) { return resources.getString(Vocabulary.Keys.Undefined); } final String name = Classes.getShortClassName(object); final StringBuilder buffer = (StringBuilder) CharSequences.camelCaseToWords(name, true); long numColors = 0; if (object instanceof IndexColorModel) { numColors = ((IndexColorModel) object).getMapSize(); } else if (object instanceof ColorModel) { final ColorModel cm = (ColorModel) object; final int[] sizes = cm.getComponentSize(); if (sizes != null) { numColors = 1; // numColorComponents should be either sizes.length or sizes.length - 1, // depending if there is an alpha channel or not. We want to ignore alpha. for (int i=cm.getNumColorComponents(); --i>=0;) { numColors *= 1L << sizes[i]; } } } if (numColors != 0) { buffer.append(" (").append(resources.getString(Vocabulary.Keys.ColorCount_1, numColors)).append(')'); } return buffer.toString().trim(); } /** * The table model for image's properties. The image can actually be any of * {@link PropertySource}, {@link RenderedImage} or {@link RenderableImage} * interface. The method {@link PropertySource#getProperty} will be invoked * only when a property is first required, in order to avoid the computation * of deferred properties before needed. If the source implements also the * {@link PropertyChangeEmitter} interface, then this table will be registered * as a listener for property changes. The changes can be emitted from any thread, * which may or may not be the <cite>Swing</cite> thread. * * @author Martin Desruisseaux (IRD) * @version 3.00 * * @since 2.3 * @module * * @todo Check for {@code WritablePropertySource} and make cells editable accordingly. */ @SuppressWarnings("serial") private static final class Table extends AbstractTableModel implements PropertyChangeListener { /** * The resources for formatting localized strings. */ private final Vocabulary resources; /** * The property sources. Usually (but not always) the same object than * {@link #changeEmitter}. May be {@code null} if no source has been set. */ private PropertySource source; /** * The property change emitter, or {@code null} if none. Usually (but not always) * the same object than {@link #source}. */ private PropertyChangeEmitter changeEmitter; /** * The properties names, or {@code null} if none. */ private String[] names; /** * Constructs a default table with no properties source. The method {@link #setSource} * must be invoked after the construction in order to display some image's properties. * * @param resources The resources for formatting localized strings. */ public Table(final Vocabulary resources) { this.resources = resources; } /** * Wraps the specified {@link RenderedImage} into a {@link PropertySource}. */ private static PropertySource wrap(final RenderedImage image) { return new PropertySource() { @Override public String[] getPropertyNames() { return image.getPropertyNames(); } @Override public String[] getPropertyNames(final String prefix) { // TODO: Not the real answer, but this method // is not needed by this Table implementation. return getPropertyNames(); } @Override public Class<?> getPropertyClass(final String name) { return null; } @Override public Object getProperty(final String name) { return image.getProperty(name); } }; } /** * Wraps the specified {@link RenderableImage} into a {@link PropertySource}. */ private static PropertySource wrap(final RenderableImage image) { return new PropertySource() { @Override public String[] getPropertyNames() { return image.getPropertyNames(); } @Override public String[] getPropertyNames(final String prefix) { // TODO: Not the real answer, but this method // is not needed by this Table implementation. return getPropertyNames(); } @Override public Class<?> getPropertyClass(final String name) { return null; } @Override public Object getProperty(final String name) { return image.getProperty(name); } }; } /** * Sets the source as a {@link PropertySource}, a {@link RenderedImage} or a * {@link RenderableImage}. If the source implements the {@link PropertyChangeEmitter} * interface, then this table will be registered as a listener for property changes. * The changes can be emitted from any thread (may or may not be the Swing thread). * * @param image The properties source, or {@code null} for removing any source. */ public void setSource(final Object image) { if (image == source) { return; } if (changeEmitter != null) { changeEmitter.removePropertyChangeListener(this); changeEmitter = null; } if (image instanceof PropertySource) { source = (PropertySource) image; } else if (image instanceof RenderedImage) { source = wrap((RenderedImage) image); } else if (image instanceof RenderableImage) { source = wrap((RenderableImage) image); } else { source = null; } names = (source!=null) ? source.getPropertyNames() : null; if (image instanceof PropertyChangeEmitter) { changeEmitter = (PropertyChangeEmitter) image; changeEmitter.addPropertyChangeListener(this); } fireTableDataChanged(); } /** * Returns the number of rows, which is equals to the number of properties. */ @Override public int getRowCount() { return (names!=null) ? names.length : 0; } /** * Returns the number of columns, which is 2 (the property name and its value). */ @Override public int getColumnCount() { return 2; } /** * Returns the column name for the given index. */ @Override public String getColumnName(final int column) { final short key; switch (column) { case 0: key=Vocabulary.Keys.Name; break; case 1: key=Vocabulary.Keys.Value; break; default: throw new IndexOutOfBoundsException(String.valueOf(column)); } return resources.getString(key); } /** * Returns the most specific superclass for all the cell values in the column. */ @Override public Class<?> getColumnClass(final int column) { switch (column) { case 0: return String.class; case 1: return Object.class; default: throw new IndexOutOfBoundsException(String.valueOf(column)); } } /** * Returns the property for the given cell. * * @param row The row index. * @param column The column index. * @return The cell value at the given index. * @throws IndexOutOfBoundsException if the row or the column is out of bounds. */ @Override public Object getValueAt(int row, int column) throws IndexOutOfBoundsException { final String name = names[row]; switch (column) { case 0: { return name; } case 1: { Object value = source.getProperty(name); if (value == Image.UndefinedProperty) { value = resources.getString(Vocabulary.Keys.Undefined); } return expandArray(value); } default: { throw new IndexOutOfBoundsException(String.valueOf(column)); } } } /** * If the specified object is an array, enumerate the array components. * Otherwise, returns the object unchanged. This method is sligtly different * than {@link java.util.Arrays#toString(Object[])} in that it expands inner * array components recursively. */ private static Object expandArray(final Object array) { if (array != null && array.getClass().isArray()) { final StringBuilder buffer = new StringBuilder(); buffer.append('{'); final int length = Array.getLength(array); for (int i=0; i<length; i++) { if (i != 0) { buffer.append(", "); } buffer.append(expandArray(Array.get(array, i))); } buffer.append('}'); return buffer.toString(); } return array; } /** * Invoked when a property changed. This method find the row for the modified * property and fire a table change event. * * @param The property change event. */ @Override public void propertyChange(final PropertyChangeEvent event) { /* * Make sure that we are running in the Swing thread. */ if (!EventQueue.isDispatchThread()) { EventQueue.invokeLater(new Runnable() { @Override public void run() { propertyChange(event); } }); return; } /* * Find the rows for the modified property, and fire a "table updated' event. */ final String name = event.getPropertyName(); int first = getRowCount(); // Past the last row. int last = -1; // Before the first row. if (name == null) { last = first-1; first = 0; } else { for (int i=first; --i>=0;) { if (names[i].equalsIgnoreCase(name)) { first = i; if (last < 0) { last = i; } } } } if (first <= last) { fireTableRowsUpdated(first, last); } } } /** * Forces the current values to be taken from the editable fields and set them as the * current values. The default implementation does nothing since there is no editable * fields in this widget. * * @since 3.12 */ @Override public void commitEdit() throws ParseException { } /** * {@inheritDoc} * * @param title The dialog box title, or {@code null} for a default title. * * @since 3.05 */ @Override public boolean showDialog(final Component owner, String title) { if (title == null) { title = Vocabulary.getResources(getLocale()).getString(Vocabulary.Keys.Properties); } return SwingUtilities.showDialog(owner, this, title); } /** * Shows the properties for the specified rendered image in a frame. * This convenience method is mostly a helper for debugging purpose. * * @param image The image to display in a frame. * * @since 3.05 */ @Debug public static void show(final RenderedImage image) { SwingUtilities.show(new ImageProperties(image), Vocabulary.format(Vocabulary.Keys.Properties)); } }