/*
* Copyright (C) 2012 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.core.gpf.ui;
import com.bc.ceres.swing.TableLayout;
import com.bc.ceres.swing.selection.SelectionChangeListener;
import com.bc.ceres.swing.selection.support.ComboBoxSelectionContext;
import org.esa.snap.core.dataio.ProductIO;
import org.esa.snap.core.dataio.ProductIOPlugInManager;
import org.esa.snap.core.dataio.ProductReaderPlugIn;
import org.esa.snap.core.datamodel.Product;
import org.esa.snap.core.datamodel.ProductFilter;
import org.esa.snap.core.datamodel.ProductManager;
import org.esa.snap.core.datamodel.ProductNode;
import org.esa.snap.core.util.StringUtils;
import org.esa.snap.core.util.SystemUtils;
import org.esa.snap.core.util.io.SnapFileFilter;
import org.esa.snap.rcp.actions.file.OpenProductAction;
import org.esa.snap.ui.AbstractDialog;
import org.esa.snap.ui.AppContext;
import org.esa.snap.ui.SnapFileChooser;
import org.openide.util.Utilities;
import javax.swing.AbstractAction;
import javax.swing.BorderFactory;
import javax.swing.DefaultComboBoxModel;
import javax.swing.DefaultListCellRenderer;
import javax.swing.JButton;
import javax.swing.JComboBox;
import javax.swing.JComponent;
import javax.swing.JFileChooser;
import javax.swing.JLabel;
import javax.swing.JList;
import javax.swing.JPanel;
import javax.swing.JPopupMenu;
import javax.swing.SwingUtilities;
import javax.swing.SwingWorker;
import javax.swing.event.PopupMenuEvent;
import javax.swing.event.PopupMenuListener;
import java.awt.BorderLayout;
import java.awt.Component;
import java.awt.Cursor;
import java.awt.Dimension;
import java.awt.Window;
import java.awt.event.ActionEvent;
import java.io.File;
import java.io.IOException;
import java.text.MessageFormat;
import java.util.Iterator;
import java.util.List;
import java.util.concurrent.ExecutionException;
/**
* WARNING: This class belongs to a preliminary API and may change in future releases.
* todo - add capability to select/add/remove multiple soures (nf - 27.11.2007)
* todo - add capability to specify optional sources
*
* @author Ralf Quast
* @version $Revision$ $Date$
*/
public class SourceProductSelector {
private AppContext appContext;
private ProductFilter productFilter;
private Product extraProduct;
private File currentDirectory;
private DefaultComboBoxModel<Object> productListModel;
private JLabel productNameLabel;
private JButton productFileChooserButton;
private JComboBox<Object> productNameComboBox;
private final ProductManager.Listener productManagerListener;
private ComboBoxSelectionContext selectionContext;
private boolean enableEmptySelection;
public SourceProductSelector(AppContext appContext) {
this(appContext, false);
}
public SourceProductSelector(AppContext appContext, boolean enableEmptySelection) {
this(appContext, "Name:", enableEmptySelection);
}
public SourceProductSelector(AppContext appContext, String labelText) {
this(appContext, labelText, false);
}
public SourceProductSelector(AppContext appContext, String labelText, boolean enableEmptySelection) {
this.appContext = appContext;
this.enableEmptySelection = enableEmptySelection;
productListModel = new DefaultComboBoxModel<>();
productNameLabel = new JLabel(labelText);
productFileChooserButton = new JButton(new ProductFileChooserAction());
final Dimension size = new Dimension(26, 16);
productFileChooserButton.setPreferredSize(size);
productFileChooserButton.setMinimumSize(size);
productNameComboBox = new JComboBox<>(productListModel);
productNameComboBox.setPrototypeDisplayValue("[1] 123456789 123456789 12345");
productNameComboBox.setRenderer(new ProductListCellRenderer());
productNameComboBox.addPopupMenuListener(new ProductPopupMenuListener());
productNameComboBox.addActionListener(e -> {
final Object selected = productNameComboBox.getSelectedItem();
if (selected != null && selected instanceof Product) {
Product product = (Product) selected;
if (product.getFileLocation() != null) {
productNameComboBox.setToolTipText(product.getFileLocation().getPath());
} else {
productNameComboBox.setToolTipText(product.getDisplayName());
}
} else {
productNameComboBox.setToolTipText("Select a source product.");
}
});
productFilter = ProductFilter.ALL;
selectionContext = new ComboBoxSelectionContext(productNameComboBox);
productManagerListener = new ProductManager.Listener() {
@Override
public void productAdded(ProductManager.Event event) {
addProduct(event.getProduct());
}
@Override
public void productRemoved(ProductManager.Event event) {
Product product = event.getProduct();
if (productListModel.getSelectedItem() == product) {
productListModel.setSelectedItem(null);
}
productListModel.removeElement(product);
}
};
}
/**
* @return the product filter, default is a filter which accepts all products
*/
public ProductFilter getProductFilter() {
return productFilter;
}
/**
* @param productFilter the product filter
*/
public void setProductFilter(ProductFilter productFilter) {
this.productFilter = productFilter;
}
public synchronized void initProducts() {
productListModel.removeAllElements();
if (enableEmptySelection) {
productListModel.addElement(null);
}
for (Product product : appContext.getProductManager().getProducts()) {
addProduct(product);
}
Product selectedProduct = appContext.getSelectedProduct();
final ProductNode productNode = Utilities.actionsGlobalContext().lookup(ProductNode.class);
if (productNode != null) {
// user would want to apply operation to the selected productNode rather than the productSceneView
selectedProduct = productNode.getProduct();
}
if (enableEmptySelection) {
productListModel.setSelectedItem(null);
} else if (selectedProduct != null && productFilter.accept(selectedProduct)) {
productListModel.setSelectedItem(selectedProduct);
}
appContext.getProductManager().addListener(productManagerListener);
}
public int getProductCount() {
if (enableEmptySelection) {
return productListModel.getSize() - 1;
} else {
return productListModel.getSize();
}
}
public void setSelectedIndex(int index) {
productListModel.setSelectedItem(productListModel.getElementAt(index));
}
public Product getSelectedProduct() {
return (Product) productListModel.getSelectedItem();
}
public void setCurrentDirectory(File directory) {
if (directory != null && directory.isDirectory()) {
currentDirectory = directory;
}
}
public File getCurrentDirectory() {
return currentDirectory;
}
public void setSelectedProduct(Product product) {
if (product == null) {
productListModel.setSelectedItem(null);
return;
}
if (productListModelContains(product)) {
productListModel.setSelectedItem(product);
} else {
if (productFilter.accept(product)) {
if (extraProduct != null) {
productListModel.removeElement(extraProduct);
extraProduct.dispose();
}
productListModel.addElement(product);
productListModel.setSelectedItem(product);
extraProduct = product;
}
}
}
public synchronized void releaseProducts() {
appContext.getProductManager().removeListener(productManagerListener);
if (extraProduct != null && getSelectedProduct() != extraProduct) {
extraProduct.dispose();
}
extraProduct = null;
productListModel.removeAllElements();
}
public void addSelectionChangeListener(SelectionChangeListener listener) {
selectionContext.addSelectionChangeListener(listener);
}
public void removeSelectionChangeListener(SelectionChangeListener listener) {
selectionContext.removeSelectionChangeListener(listener);
}
private void addProduct(Product product) {
if (productFilter.accept(product)) {
productListModel.addElement(product);
}
}
// UI Components
/////////////////////////////////////
public JComboBox<Object> getProductNameComboBox() {
return productNameComboBox;
}
public JLabel getProductNameLabel() {
return productNameLabel;
}
public JButton getProductFileChooserButton() {
return productFileChooserButton;
}
private boolean productListModelContains(Product product) {
for (int i = 0; i < productListModel.getSize(); i++) {
Object listProduct = productListModel.getElementAt(i);
if (listProduct == null) {
continue;
}
if (listProduct.equals(product)) {
return true;
}
}
return false;
}
public JPanel createDefaultPanel() {
return createDefaultPanel("Source Product");
}
public JPanel createDefaultPanel(String borderTitle) {
final JPanel subPanel = new JPanel(new BorderLayout(3, 3));
subPanel.add(getProductNameComboBox(), BorderLayout.CENTER);
subPanel.add(getProductFileChooserButton(), BorderLayout.EAST);
final TableLayout tableLayout = new TableLayout(1);
tableLayout.setTableAnchor(TableLayout.Anchor.WEST);
tableLayout.setTableWeightX(1.0);
tableLayout.setRowFill(0, TableLayout.Fill.HORIZONTAL);
tableLayout.setRowFill(1, TableLayout.Fill.HORIZONTAL);
tableLayout.setTablePadding(3, 3);
JPanel panel = new JPanel(tableLayout);
panel.add(getProductNameLabel());
panel.add(subPanel);
if (StringUtils.isNotNullAndNotEmpty(borderTitle)) {
panel.setBorder(BorderFactory.createTitledBorder(borderTitle));
panel.add(tableLayout.createVerticalSpacer());
}
return panel;
}
private class ProductFileChooserAction extends AbstractAction {
private String APPROVE_BUTTON_TEXT = "Select";
private JFileChooser chooser;
private ProductFileChooserAction() {
super("...");
chooser = new SnapFileChooser();
chooser.setDialogTitle("Select Source Product");
final Iterator<ProductReaderPlugIn> iterator = ProductIOPlugInManager.getInstance().getAllReaderPlugIns();
List<SnapFileFilter> sortedFileFilters = SnapFileFilter.getSortedFileFilters(iterator);
for (SnapFileFilter fileFilter : sortedFileFilters) {
chooser.addChoosableFileFilter(fileFilter);
}
chooser.setAcceptAllFileFilterUsed(true);
chooser.setFileFilter(chooser.getAcceptAllFileFilter());
}
@Override
public void actionPerformed(ActionEvent event) {
final Window window = SwingUtilities.getWindowAncestor((JComponent) event.getSource());
String homeDirPath = SystemUtils.getUserHomeDir().getPath();
String openDir = appContext.getPreferences().getPropertyString(OpenProductAction.PREFERENCES_KEY_LAST_PRODUCT_DIR,
homeDirPath);
currentDirectory = new File(openDir);
chooser.setCurrentDirectory(currentDirectory);
if (chooser.showDialog(window, APPROVE_BUTTON_TEXT) == JFileChooser.APPROVE_OPTION) {
SwingWorker cursorWorker = new CursorWorker(window, chooser);
cursorWorker.execute();
currentDirectory = chooser.getCurrentDirectory();
appContext.getPreferences().setPropertyString(OpenProductAction.PREFERENCES_KEY_LAST_PRODUCT_DIR,
currentDirectory.getAbsolutePath());
}
}
}
private class CursorWorker extends SwingWorker<Product, Void> {
private final Component window;
private final JFileChooser chooser;
private Cursor defaultCursor;
private File file;
public CursorWorker(Component window, JFileChooser chooser) {
this.window = window;
this.chooser = chooser;
}
@Override
protected Product doInBackground() throws Exception {
Product product = null;
try {
defaultCursor = window.getCursor();
window.setCursor(Cursor.getPredefinedCursor(Cursor.WAIT_CURSOR));
file = chooser.getSelectedFile();
product = ProductIO.readProduct(file);
} catch (IOException e) {
handleError(e.getMessage());
}
return product;
}
@Override
protected void done() {
Product product = null;
try {
product = get();
if (product == null) {
throw new IOException(MessageFormat.format("File ''{0}'' could not be read.", this.file.getPath()));
}
if (productFilter.accept(product)) {
setSelectedProduct(product);
} else {
final String message = String.format("Product [%s] is not a valid source.",
product.getFileLocation().getCanonicalPath());
handleError(message);
product.dispose();
}
} catch (InterruptedException | IOException | ExecutionException e) {
if (product != null) {
product.dispose();
}
handleError(e.getMessage());
e.printStackTrace();
} finally {
window.setCursor(defaultCursor);
}
}
private void handleError(final String message) {
SwingUtilities.invokeLater(() -> {
AbstractDialog.showWarningDialog(window, message, "Error");
});
}
}
private static class ProductListCellRenderer extends DefaultListCellRenderer {
@Override
public Component getListCellRendererComponent(JList list, Object value, int index, boolean isSelected,
boolean cellHasFocus) {
final Component cellRendererComponent =
super.getListCellRendererComponent(list, value, index, isSelected, cellHasFocus);
if (cellRendererComponent instanceof JLabel) {
final JLabel label = (JLabel) cellRendererComponent;
if (value instanceof Product) {
final Product product = (Product) value;
label.setText(product.getDisplayName());
} else {
label.setText(" ");
}
}
return cellRendererComponent;
}
}
/**
* To let the popup menu be wider than the closed combobox.
* Adapted an idea from http://bugs.sun.com/bugdatabase/view_bug.do?bug_id=6257236
*/
private static class ProductPopupMenuListener implements PopupMenuListener {
@Override
public void popupMenuWillBecomeVisible(PopupMenuEvent e) {
JComboBox box = (JComboBox) e.getSource();
Object comp = box.getUI().getAccessibleChild(box, 0);
if (!(comp instanceof JPopupMenu)) {
return;
}
JComponent scrollPane = (JComponent) ((JPopupMenu) comp).getComponent(0);
Dimension size = new Dimension();
size.width = scrollPane.getPreferredSize().width;
final int boxItemCount = box.getModel().getSize();
for (int i = 0; i < boxItemCount; i++) {
final JLabel label = new JLabel();
Object elementAt = box.getModel().getElementAt(i);
if (elementAt != null && elementAt instanceof Product) {
label.setText(((Product) elementAt).getDisplayName());
}
size.width = Math.max(label.getPreferredSize().width, size.width);
}
size.height = scrollPane.getPreferredSize().height;
scrollPane.setPreferredSize(size);
scrollPane.setMaximumSize(size);
}
@Override
public void popupMenuCanceled(PopupMenuEvent e) {
}
@Override
public void popupMenuWillBecomeInvisible(PopupMenuEvent e) {
}
}
}