/*
* Geotoolkit.org - An Open Source Java GIS Toolkit
* http://www.geotoolkit.org
*
* (C) 2009-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.Map;
import java.util.Set;
import java.util.List;
import java.util.Locale;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Iterator;
import java.util.ArrayList;
import java.util.Comparator;
import java.util.Collections;
import java.io.File;
import java.io.FileReader;
import java.io.IOException;
import java.io.BufferedReader;
import javax.imageio.spi.IIORegistry;
import javax.imageio.spi.ImageReaderSpi;
import javax.imageio.spi.ImageWriterSpi;
import javax.imageio.spi.ImageReaderWriterSpi;
import java.awt.Component;
import java.awt.Container;
import java.awt.HeadlessException;
import javax.swing.JDialog;
import javax.swing.JSplitPane;
import javax.swing.JFileChooser;
import javax.swing.filechooser.FileFilter;
import javax.swing.filechooser.FileNameExtensionFilter;
import java.beans.PropertyChangeEvent;
import java.beans.PropertyChangeListener;
import org.apache.sis.util.ArraysExt;
import org.apache.sis.util.logging.Logging;
import org.geotoolkit.resources.Vocabulary;
import org.geotoolkit.resources.Errors;
/**
* A file chooser for images. Compared to the standard {@code JFileChooser}, this class provides
* the following additional functionalities:
* <p>
* <ul>
* <li>The list of {@linkplain FileFilter file filters} is determined automatically from
* the image formats known to {@link IIORegistry}.</li>
* <li>An optional pseudo-format called "<cite>List of files</cite>"
* {@linkplain #isListFileFilterUsed() can be added}. This pseudo-format allows the
* user to specify a text file listing the paths to many image files.</li>
* <li>The {@code showDialog(...)} methods display an {@link ImageFileProperties} pane
* at the right of the {@code ImageFileChooser}.</li>
* </ul>
* <p>
* This class should typically be used as below (replace "{@code showOpenDialog}" by
* "{@code showSaveDialog"} for saving an image instead than loading it):
*
* {@preformat java
* ImageFileChooser chooser = new ImageFileChooser("png", true);
* if (chooser.showOpenDialog(parent) == ImageFileChooser.APPROVE_OPTION) {
* File selected = chooser.getSelectedFile();
* }
* }
*
* @author Martin Desruisseaux (Geomatys)
* @version 3.08
*
* @see ImageFileProperties
*
* @since 3.00
* @module
*/
@SuppressWarnings("serial")
public class ImageFileChooser extends JFileChooser {
/**
* The default format, or {@code null} if none.
*/
private String defaultFormat;
/**
* Non-null if the list of providers should include a special entry
* for loading a text file which contains a list of files.
* <p>
* We can not serialize this field, because {@link FileNameExtensionFilter}
* is not serializable.
*/
private transient FileFilter listFileFilter;
/**
* {@code true} if the list of providers should include a special entry
* for loading a text file which contains a list of files.
*/
private boolean listFileFilterUsed;
/**
* Selected files, cached for avoiding to parse the same "list of files" twice.
*/
private transient File[] selectedFiles;
/**
* The providers for each file format listed in the file filter.
*/
private final Map<FileFilter,ImageReaderWriterSpi> providers;
/**
* The panel showing the properties of the selected file, or {@code null} if none.
*
* @since 3.05
*/
private ImageFileProperties propertiesPane;
/**
* Creates a new file chooser having the user's default directory as the initial directory.
* This default depends on the operating system. It is typically the "<cite>My Documents</cite>"
* folder on Windows, and the user's home directory on Unix.
* <p>
* The {@link #setDialogType(int)} method will be invoked implicitly by the
* {@link #showOpenDialog showOpenDialog} and {@link #showSaveDialog showSaveDialog} methods.
* If those methods are not going to be invoked, then callers should invoke
* {@code setDialogType(int)} explicitly after construction in order to
* {@linkplain #addChoosableFileFilter add file filters} appropriate for
* the kind of operation (<cite>open</cite> or <cite>save</cite>) to be performed.
*
* @param defaultFormat The default format to be initially selected, or {@code null}
* for proposing all formats. If non-null, it should be an Image I/O format name
* like {@code "png"} or {@code "jpeg"}.
*/
public ImageFileChooser(final String defaultFormat) {
this(defaultFormat, false);
}
/**
* Creates a new file chooser, optionally with an {@link ImageFileProperties} pane to be shown.
* If {@code showProperties} is {@code true}, then the properties pane will be visible when the
* {@link #showDialog showDialog} method or one of its variants is invoked. The properties pane
* is not visible otherwise; see {@link #setPropertiesPane(ImageFileProperties)} for more
* information.
*
* @param defaultFormat The default format to be initially selected, or {@code null} for all.
* @param showProperties {@code true} for creating an {@link ImageFileProperties}. The default
* value is {@code false}.
*
* @since 3.05
*/
public ImageFileChooser(final String defaultFormat, final boolean showProperties) {
super();
this.defaultFormat = defaultFormat;
providers = new HashMap<>();
addPropertyChangeListener(SELECTED_FILES_CHANGED_PROPERTY, new PropertyChangeListener() {
@Override public void propertyChange(PropertyChangeEvent event) {
selectedFiles = null;
}
});
if (showProperties) {
setPropertiesPane(new ImageFileProperties());
}
}
/**
* Returns {@code true} if this {@code ImageFileChooser} should proposes a filter
* for list of files. A "list of files" is a file with {@code .txt}, {@code .lst}
* or {@code .csv} extension which contains the actual list of images to select,
* which may be spread over many directories.
* <p>
* By default this method returns {@code false}.
*
* @return {@code true} if this chooser should proposes the selection of {@code .txt},
* {@code .lst} or {@code .csv} files that contain a list of image files.
*
* @see #isAcceptAllFileFilterUsed
*/
public boolean isListFileFilterUsed() {
return listFileFilterUsed;
}
/**
* Sets whatever this {@code ImageFileChooser} should proposes a filter for list of files.
*
* @param enabled {@code true} if this chooser should proposes the selection of {@code .txt},
* {@code .lst} or {@code .csv} files that contain a list of image files.
*
* @see #setAcceptAllFileFilterUsed
*/
public void setListFileFilterUsed(final boolean enabled) {
final boolean old = listFileFilterUsed;
listFileFilterUsed = enabled;
firePropertyChange("listFileFilterUsed", old, enabled);
}
/**
* Resets the choosable {@linkplain FileFilter file filter} list to its starting state.
*/
@Override
public void resetChoosableFileFilters() {
super.resetChoosableFileFilters();
providers.clear();
}
/**
* Returns the pane showing the properties of the selected file, or {@code null} if none.
* This is different than the {@linkplain #getAccessory() accessory} pane provided by
* {@code JFileChooser} in that this pane is located at the right side of the chooser,
* because it is too big for fitting in the accessory area of the file chooser.
*
* @return The current pane showing image properties, or {@code null} if none.
*
* @see #getAccessory()
*
* @since 3.05
*/
public ImageFileProperties getPropertiesPane() {
return propertiesPane;
}
/**
* Sets the pane showing the properties of the selected file, which can be {@code null}
* for hiding the pane. This method automatically {@link #removePropertyChangeListener
* unregister} the old pane (if any) from the list of property change listeners, and
* {@linkplain #addPropertyChangeListener register} the new pane (if non null) instead.
* <p>
* The properties pane is shown when one of the {@link #showDialog showDialog} method
* variants is invoked. If those methods are not going to be used for showing this file
* chooser, then the caller shall adds the {@code ImageFileProperties} pane himself in
* his own pane.
*
* @param properties The new pane showing image properties, or {@code null} if none.
*
* @see #setAccessory(JComponent)
*
* @since 3.05
*/
public void setPropertiesPane(final ImageFileProperties properties) {
final ImageFileProperties old = propertiesPane;
if (old != null) {
removePropertyChangeListener(SELECTED_FILE_CHANGED_PROPERTY, old);
}
if (properties != null) {
addPropertyChangeListener(SELECTED_FILE_CHANGED_PROPERTY, properties);
}
propertiesPane = properties;
firePropertyChange("propertiesPane", old, properties);
}
/**
* Sets whatever this dialog is going to be used for reading or writing images. This method
* resets the {@linkplain FileFilter file filters} to all image formats registered in
* {@link IIORegistry}. Only formats available for reading or writing (depending on the value
* of the {@code mode} argument) will be listed.
*
* @param mode {@link #OPEN_DIALOG OPEN_DIALOG} for a chooser to be used for reading images, or
* {@link #SAVE_DIALOG SAVE_DIALOG} for a chooser to be used for writing images.
*/
@Override
public void setDialogType(final int mode) {
super.setDialogType(mode);
final Locale locale = getLocale();
final Class<? extends ImageReaderWriterSpi> category;
switch (mode) {
case OPEN_DIALOG: category = ImageReaderSpi.class; break;
case SAVE_DIALOG: category = ImageWriterSpi.class; break;
case CUSTOM_DIALOG: resetChoosableFileFilters(); return;
default: throw new IllegalArgumentException(Errors.format(
Errors.Keys.IllegalArgument_2, "mode", mode));
}
final IIORegistry registry = IIORegistry.getDefaultInstance();
final Iterator<? extends ImageReaderWriterSpi> it = registry.getServiceProviders(category, true);
final List<FileFilter> filters = new ArrayList<>();
final Map<String,String> suffixDone = new HashMap<>();
final Set<String> formatsDone = new HashSet<>();
final StringBuilder buffer = new StringBuilder();
resetChoosableFileFilters();
FileFilter preferred = null;
skip: while (it.hasNext()) {
boolean isPreferred = false;
final ImageReaderWriterSpi spi = it.next();
String longFormat = null;
for (final String format : spi.getFormatNames()) {
if (!formatsDone.add(format)) {
// Avoid declaring the same format twice (e.g. declaring
// both the JSE and JAI ImageReaders for the PNG format).
continue skip;
}
if (defaultFormat != null && defaultFormat.equalsIgnoreCase(format)) {
isPreferred = true;
}
// Remember the longest format string. If two of them
// have the same length, favor the one in upper case.
longFormat = ImageFormatEntry.longest(longFormat, format);
}
if (longFormat == null) {
longFormat = spi.getDescription(locale);
}
/*
* At this point, we have a provider to take in account. We need to get the list of
* suffixes, but we don't need both the lower-case and upper-case flavors of the same
* suffix. If those two flavors exist, then we will keep only the first one (which is
* usually the lower-case flavor). The iteration is performed in reverse order for that
* reason.
*/
String[] suffix = spi.getFileSuffixes();
for (int i=suffix.length; --i >= 0;) {
final String s = suffix[i].trim();
if (!s.isEmpty()) {
suffixDone.put(s.toLowerCase(locale), s);
}
}
if (!suffixDone.isEmpty()) {
suffix = suffixDone.values().toArray(new String[suffixDone.size()]);
suffixDone.clear();
buffer.setLength(0);
buffer.append(longFormat);
String separator = " (";
for (final String s : suffix) {
buffer.append(separator).append("*.").append(s);
separator = ", ";
}
buffer.append(')');
final FileFilter filter = new FileNameExtensionFilter(buffer.toString(), suffix);
filters.add(filter);
providers.put(filter, spi);
if (isPreferred) {
preferred = filter;
}
}
}
/*
* Sorts the filters in alphabetical order before to add them to JFileChooser.
*/
Collections.sort(filters, new Comparator<FileFilter>() {
@Override public int compare(final FileFilter f1, final FileFilter f2) {
return f1.getDescription().compareTo(f2.getDescription());
}
});
/*
* Adds the file filter for "file containing list of files" if the user allowed it.
*/
if (isListFileFilterUsed()) {
final Vocabulary resources = Vocabulary.getResources(getLocale());
listFileFilter = new FileNameExtensionFilter(resources.getString(
Vocabulary.Keys.ImageList), "txt", "lst", "csv");
filters.add(listFileFilter);
}
for (final FileFilter filter : filters) {
addChoosableFileFilter(filter);
}
setFileFilter(preferred); // Null is okay.
}
/**
* Returns the selected file. If the user has selected a file which contains a list of images
* (as proposed if {@code setListFileFilterUsed(true)} has been invoked), then this method
* returns the list file itself, not its content since this method can only returns a single
* file.
*/
@Override
public File getSelectedFile() {
return super.getSelectedFile();
}
/**
* Returns the selected file. If the user has selected a file which contains a list of images
* (as proposed if {@code setListFileFilterUsed(true)} has been invoked), then this method
* opens that file using the platform encoding and returns its content. If an I/O error occurred
* while reading that file, then its content is not included in the returned array.
*
* @return The list of selected files, including the content of text files that are
* list of images.
*/
@Override
public File[] getSelectedFiles() {
final FileFilter filter = listFileFilter;
if (filter == null || !filter.equals(getFileFilter())) {
File[] files = super.getSelectedFiles();
if (files == null || files.length == 0) {
final File file = getSelectedFile();
if (file != null) {
files = new File[] {file};
}
}
return files;
}
/*
* At this point, the selected files (usually only 1) are actually text files
* which contain a list of images. We need to parse the content of those files.
*/
if (selectedFiles == null) {
final File directory = getCurrentDirectory();
final List<File> content = new ArrayList<>();
for (final File list : super.getSelectedFiles()) {
if (!filter.accept(list)) {
content.add(list);
} else try (BufferedReader in = new BufferedReader(new FileReader(list))) {
String line; while ((line = in.readLine()) != null) {
line = line.trim();
if (!line.isEmpty() && line.charAt(0) != '#') {
File file = new File(line);
if (!file.isAbsolute()) {
file = new File(directory, line);
}
content.add(file);
}
}
} catch (IOException e) {
Logging.unexpectedException(null, ImageFileChooser.class, "getSelectedFiles", e);
}
}
selectedFiles = content.toArray(new File[content.size()]);
}
return selectedFiles.clone();
}
/**
* Returns the image reader/writer provider for the {@linkplain #getFileFilter() currently
* selected file filter}. If the {@linkplain #setDialogType(int) dialog type} has been set
* to {@link #OPEN_DIALOG OPEN_DIALOG}, then this method returns either {@code null} or an
* instance of {@link ImageReaderSpi}. Otherwise if the dialog type has been set to {@link
* #SAVE_DIALOG SAVE_DIALOG}, then this method returns either {@code null} or an instance
* of {@link ImageWriterSpi}.
*
* @return The image reader/writer provider for the currently selected file filter,
* or {@code null} if the current file filter is unknown to this method.
*/
public ImageReaderWriterSpi getCurrentProvider() {
ImageReaderWriterSpi provider = providers.get(getFileFilter());
if (provider == null) {
/*
* Before to gives up, checks if all currently selected files could be read using
* the same provider. This check is necessary since the selected files may actually
* come from a selected text file which contains a list of images.
*/
String[] suffixes = null;
verify: for (final File file : getSelectedFiles()) {
String ext = file.getName();
int s = ext.lastIndexOf('.');
if (s <= 0) {
// No extension - conservatively said that we don't know the provider.
return null;
}
ext = ext.substring(s+1);
if (suffixes == null) {
for (final ImageReaderWriterSpi candidate : providers.values()) {
suffixes = candidate.getFileSuffixes();
if (ArraysExt.containsIgnoreCase(suffixes, ext)) {
provider = candidate;
continue verify;
}
}
return null;
}
if (!ArraysExt.containsIgnoreCase(suffixes, ext)) {
// Found image that would require a different provider.
return null;
}
}
}
return provider;
}
/**
* Creates and returns a new dialog wrapping this {@code ImageFileChooser}, optionally
* with its {@link ImageFileProperties} pane. This method is invoked automatically by
* the {@link #showDialog showDialog} methods and is overridden for adding the optional
* properties pane, if presents.
*
* @param parent he parent component of the dialog, or {@code null}.
* @return A dialog containing this file chooser, and optionally a properties pane.
* @throws HeadlessException If the graphics environment is headlesss.
*
* @since 3.05
*/
@Override
protected JDialog createDialog(final Component parent) throws HeadlessException {
final JDialog dialog = super.createDialog(parent);
if (propertiesPane != null) {
synchronized (dialog.getTreeLock()) {
final Container contentPane = dialog.getContentPane();
Component chooser = contentPane;
if (contentPane.getComponentCount() == 1) {
chooser = contentPane.getComponent(0);
contentPane.remove(0);
}
dialog.setContentPane(new JSplitPane(
JSplitPane.HORIZONTAL_SPLIT, true, chooser, propertiesPane));
}
/*
* Add the file chooser and the properties panel with constraints choosed in
* such a way that if the dialog box is resized, only the properties panel
* will be resized accordingly.
*/
dialog.pack();
dialog.setLocationRelativeTo(parent);
}
return dialog;
}
}