/*
* Copyright (C) 2010 Brockmann Consult GmbH (info@brockmann-consult.de)
*
* This program is free software; you can redistribute it and/or modify it
* under the terms of the GNU General Public License as published by the Free
* Software Foundation; either version 3 of the License, or (at your option)
* any later version.
* This program 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 General Public License for
* more details.
*
* You should have received a copy of the GNU General Public License along
* with this program; if not, see http://www.gnu.org/licenses/
*/
package org.esa.snap.ui;
import com.bc.ceres.binding.ConversionException;
import org.esa.snap.core.util.Debug;
import org.esa.snap.core.util.SystemUtils;
import org.esa.snap.core.util.converters.RectangleConverter;
import org.esa.snap.core.util.io.FileUtils;
import org.esa.snap.core.util.io.SnapFileFilter;
import org.esa.snap.runtime.Config;
import sun.swing.FilePane;
import javax.swing.Icon;
import javax.swing.ImageIcon;
import javax.swing.JDialog;
import javax.swing.JFileChooser;
import javax.swing.filechooser.FileFilter;
import javax.swing.filechooser.FileNameExtensionFilter;
import javax.swing.filechooser.FileSystemView;
import java.awt.Component;
import java.awt.Container;
import java.awt.Graphics;
import java.awt.GraphicsDevice;
import java.awt.GraphicsEnvironment;
import java.awt.HeadlessException;
import java.awt.Rectangle;
import java.awt.Window;
import java.awt.event.ComponentAdapter;
import java.awt.event.ComponentEvent;
import java.awt.event.WindowAdapter;
import java.awt.event.WindowEvent;
import java.io.File;
import java.util.logging.Level;
import java.util.prefs.BackingStoreException;
import java.util.prefs.Preferences;
/**
* The general SNAP file chooser.
*
* @author Norman Fomferra
*/
public class SnapFileChooser extends JFileChooser {
private static final Object syncFileFiltersObject = new Object();
private static final String PREFERENCES_BOUNDS_OPEN = "snap.fileChooser.dialogBounds.open";
private static final String PREFERENCES_BOUNDS_SAVE = "snap.fileChooser.dialogBounds.save";
private static final String PREFERENCES_BOUNDS_CUSTOM = "snap.fileChooser.dialogBounds.custom";
private static final String PREFERENCES_VIEW_TYPE = "snap.fileChooser.viewType";
private final ResizeHandler resizeHandler;
private final CloseHandler windowCloseHandler;
private final Preferences snapPreferences;
private String lastFilename;
private Rectangle dialogBounds;
public SnapFileChooser() {
this(null, null);
}
public SnapFileChooser(FileSystemView fsv) {
this(null, fsv);
}
public SnapFileChooser(File currentDirectory) {
this(currentDirectory, null);
}
public SnapFileChooser(File currentDirectory, FileSystemView fsv) {
super(currentDirectory, fsv);
snapPreferences = Config.instance("snap").preferences();
resizeHandler = new ResizeHandler();
windowCloseHandler = new CloseHandler();
init();
}
@Override
public Icon getIcon(File f) {
Icon icon = null;
if (f != null) {
icon = super.getIcon(f);
if (f.isDirectory() && isCompoundDocument(f)) {
return new CompoundDocumentIcon(icon);
}
}
return icon;
}
@Override
public boolean isTraversable(File f) {
return f.isDirectory() && !isCompoundDocument(f);
}
/**
* Overridden in order to recognize dialog size changes.
*
* @param parent the parent
* @return the dialog
*
* @throws HeadlessException if GraphicsEnvironment.isHeadless() returns true.
*/
@Override
protected JDialog createDialog(Component parent) throws HeadlessException {
final JDialog dialog = super.createDialog(parent);
Rectangle dialogBounds = loadDialogBounds();
if (dialogBounds != null) {
dialog.setBounds(dialogBounds);
}
dialog.addComponentListener(resizeHandler);
dialog.addWindowListener(windowCloseHandler);
initViewType();
return dialog;
}
/**
* Called by the UI when the user hits the Approve button (labeled "Open" or "Save", by default). This can also be
* called by the programmer.
*/
@Override
public void approveSelection() {
Debug.trace("SnapFileChooser: approveSelection(): selectedFile = " + getSelectedFile());
Debug.trace("SnapFileChooser: approveSelection(): currentFilename = " + getCurrentFilename());
Debug.trace("SnapFileChooser: approveSelection(): currentDirectory = " + getCurrentDirectory());
if (getDialogType() != JFileChooser.OPEN_DIALOG) {
ensureSelectedFileHasValidExtension();
}
super.approveSelection();
}
/**
* Gets the dialog bounds to be used for the next {@link #showDialog(java.awt.Component, String)} call.
*
* @return the dialog bounds
*/
public Rectangle getDialogBounds() {
return dialogBounds;
}
/**
* Sets the dialog bounds to be used for the next {@link #showDialog(java.awt.Component, String)} call.
*
* @param rectangle the dialog bounds
*/
public void setDialogBounds(Rectangle rectangle) {
this.dialogBounds = rectangle;
storeDialogBounds(dialogBounds);
}
/**
* @return The current filename, or {@code null}.
*/
public String getCurrentFilename() {
File selectedFile = getSelectedFile();
if (selectedFile != null) {
return selectedFile.getName();
}
return null;
}
/**
* Sets the current filename.
*
* @param currentFilename The current filename, or {@code null}.
*/
public void setCurrentFilename(String currentFilename) {
Debug.trace("SnapFileChooser: setCurrentFilename(\"" + currentFilename + "\")");
String defaultExtension = getDefaultExtension();
if (getDialogType() != JFileChooser.OPEN_DIALOG) {
if (currentFilename != null && defaultExtension != null) {
FileFilter fileFilter = getFileFilter();
if (fileFilter instanceof SnapFileFilter) {
SnapFileFilter filter = (SnapFileFilter) fileFilter;
if (!filter.checkExtension(currentFilename)) {
currentFilename = FileUtils.exchangeExtension(currentFilename, defaultExtension);
}
} else if (fileFilter instanceof FileNameExtensionFilter) {
FileNameExtensionFilter filter = (FileNameExtensionFilter) fileFilter;
if (!SnapFileFilter.checkExtensions(currentFilename, filter.getExtensions())) {
currentFilename = FileUtils.exchangeExtension(currentFilename, defaultExtension);
}
}
}
}
if (currentFilename != null && currentFilename.length() > 0) {
setSelectedFile(new File(getCurrentDirectory(), currentFilename));
}
}
/**
* Returns the currently selected SNAP file filter.
*
* @return the current SNAP file filter, or {@code null}
*/
public SnapFileFilter getSnapFileFilter() {
FileFilter ff = getFileFilter();
if (ff instanceof SnapFileFilter) {
return (SnapFileFilter) ff;
}
return null;
}
/**
* @return The current extension or {@code null} if it is unknown.
*/
public String getDefaultExtension() {
if (getSnapFileFilter() != null) {
return getSnapFileFilter().getDefaultExtension();
}
return null;
}
/**
* Checks whether or not the given filename with one of the known file extensions. The known file extension of this
* file chooser are those, which are registered using a {@link SnapFileFilter}.
*
* @param filename the filename to be checked
* @return {@code true}, if the given file has a "known" extension
*
* @see SnapFileFilter
*/
public boolean checkExtension(String filename) {
if (filename != null) {
FileFilter[] fileFilters = getChoosableFileFilters();
if (fileFilters != null) {
for (FileFilter filter : fileFilters) {
if (filter instanceof SnapFileFilter) {
final SnapFileFilter snapFileFilter = (SnapFileFilter) filter;
if (snapFileFilter.checkExtension(filename)) {
return true;
}
}
}
}
}
return false;
}
@Override
public FileFilter[] getChoosableFileFilters() {
synchronized (syncFileFiltersObject) {
return super.getChoosableFileFilters();
}
}
@Override
public void addChoosableFileFilter(FileFilter filter) {
synchronized (syncFileFiltersObject) {
super.addChoosableFileFilter(filter);
}
}
/**
* Utility method which returns this file chooser's parent window.
*
* @return the parent window or {@code null}
*/
protected Window getWindow() {
Container w = this;
while (!(w instanceof Window)) {
w = w.getParent();
}
return (Window) w;
}
///////////////////////////////////////////////////////////////////////////
// private stuff
///////////////////////////////////////////////////////////////////////////
private void init() {
setAcceptAllFileFilterUsed(false);
addPropertyChangeListener(JFileChooser.SELECTED_FILE_CHANGED_PROPERTY, evt -> {
Object newValue = evt.getNewValue();
if (newValue instanceof File) {
lastFilename = ((File) newValue).getName();
}
});
addPropertyChangeListener(JFileChooser.FILE_FILTER_CHANGED_PROPERTY, evt -> {
final SnapFileFilter snapFileFilter = getSnapFileFilter();
if (snapFileFilter != null) {
setFileSelectionMode(snapFileFilter.getFileSelectionMode().getValue());
} else {
setFileSelectionMode(FILES_ONLY);
}
if (getSelectedFile() != null) {
return;
}
if (lastFilename == null || lastFilename.length() == 0) {
return;
}
setCurrentFilename(lastFilename);
});
}
private boolean isCompoundDocument(File file) {
final FileFilter[] filters = getChoosableFileFilters();
for (FileFilter fileFilter : filters) {
if (fileFilter instanceof SnapFileFilter) {
SnapFileFilter snapFileFilter = (SnapFileFilter) fileFilter;
if (snapFileFilter.isCompoundDocument(file)) {
return true;
}
}
}
return false;
}
private void ensureSelectedFileHasValidExtension() {
File selectedFile = getSelectedFile();
if (selectedFile != null) {
SnapFileFilter mff = getSnapFileFilter();
if (mff != null
&& mff.getDefaultExtension() != null
&& !mff.checkExtension(selectedFile)) {
selectedFile = FileUtils.exchangeExtension(selectedFile, mff.getDefaultExtension());
Debug.trace("mod. selected file: " + selectedFile.getPath());
setSelectedFile(selectedFile);
}
}
}
private void storeDialogBounds(Rectangle bounds) {
int dialogType = getDialogType();
switch (dialogType) {
case OPEN_DIALOG:
putRectangleToPreferences(PREFERENCES_BOUNDS_OPEN, bounds);
break;
case SAVE_DIALOG:
putRectangleToPreferences(PREFERENCES_BOUNDS_SAVE, bounds);
break;
case CUSTOM_DIALOG:
default:
putRectangleToPreferences(PREFERENCES_BOUNDS_CUSTOM, bounds);
}
}
private Rectangle loadDialogBounds() {
switch (getDialogType()) {
case OPEN_DIALOG:
return getRectangleFromPreferences(PREFERENCES_BOUNDS_OPEN);
case SAVE_DIALOG:
return getRectangleFromPreferences(PREFERENCES_BOUNDS_SAVE);
case CUSTOM_DIALOG:
default:
return getRectangleFromPreferences(PREFERENCES_BOUNDS_CUSTOM);
}
}
private void putRectangleToPreferences(String key, Rectangle bounds) {
snapPreferences.put(key, new RectangleConverter().format(bounds));
}
private Rectangle getRectangleFromPreferences(String key) {
String rectString = snapPreferences.get(key, null);
if (rectString != null) {
try {
Rectangle rectangle = new RectangleConverter().parse(rectString);
GraphicsDevice[] screenDevices = GraphicsEnvironment.getLocalGraphicsEnvironment().getScreenDevices();
for (GraphicsDevice screenDevice : screenDevices) {
if (screenDevice.getDefaultConfiguration().getBounds().contains(rectangle.getLocation())) {
return rectangle;
}
}
} catch (ConversionException e) {
SystemUtils.LOG.log(Level.WARNING, "Not able to parse preferences value: " + rectString, e);
}
}
return null;
}
private void initViewType() {
FilePane filePane = findFilePane(this);
if(filePane != null) {
int viewType = snapPreferences.getInt(PREFERENCES_VIEW_TYPE, FilePane.VIEWTYPE_LIST);
filePane.setViewType(viewType);
}
}
private FilePane findFilePane(Container root) {
Component[] components = root.getComponents();
for (Component component : components) {
if (component instanceof FilePane) {
return (FilePane) component;
}
if(component instanceof Container) {
FilePane filePane = findFilePane((Container) component);
if (filePane != null) {
return filePane;
}
}
}
return null;
}
private class ResizeHandler extends ComponentAdapter {
@Override
public void componentMoved(ComponentEvent e) {
setDialogBounds(e.getComponent().getBounds());
}
@Override
public void componentResized(ComponentEvent e) {
setDialogBounds(e.getComponent().getBounds());
}
}
private static class CompoundDocumentIcon implements Icon {
private final Icon baseIcon;
private static final Icon compoundDocumentIcon = new ImageIcon(CompoundDocumentIcon.class.getResource("CompoundDocument12.png"));
public CompoundDocumentIcon(Icon baseIcon) {
this.baseIcon = baseIcon;
}
@Override
public void paintIcon(Component c, Graphics g, int x, int y) {
baseIcon.paintIcon(c, g, x, y);
compoundDocumentIcon.paintIcon(c, g,
x + baseIcon.getIconWidth() - compoundDocumentIcon.getIconWidth(),
y + baseIcon.getIconHeight() - compoundDocumentIcon.getIconHeight());
}
@Override
public int getIconWidth() {
return baseIcon.getIconWidth();
}
@Override
public int getIconHeight() {
return baseIcon.getIconHeight();
}
}
private class CloseHandler extends WindowAdapter {
@Override
public void windowClosed(WindowEvent e) {
FilePane filePane = findFilePane(SnapFileChooser.this);
if (filePane != null) {
snapPreferences.putInt(PREFERENCES_VIEW_TYPE, filePane.getViewType());
flushPreferences();
}
}
private void flushPreferences() {
try {
snapPreferences.flush();
} catch (BackingStoreException bse) {
SystemUtils.LOG.severe("Could not store preferences: " + bse.getMessage());
}
}
}
}