/* * Copyright (C) 2014 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.product; import com.bc.ceres.binding.Property; import com.bc.ceres.binding.ValidationException; import com.bc.ceres.core.Assert; import com.bc.ceres.swing.binding.ComponentAdapter; import org.esa.snap.core.datamodel.Product; import org.esa.snap.core.datamodel.ProductFilter; import org.esa.snap.core.datamodel.ProductNode; import org.esa.snap.core.util.Debug; import org.esa.snap.ui.AppContext; import org.esa.snap.ui.UIUtils; import org.esa.snap.ui.tool.ToolButtonFactory; import javax.swing.AbstractButton; import javax.swing.BoxLayout; import javax.swing.DefaultListCellRenderer; import javax.swing.JComponent; import javax.swing.JLabel; import javax.swing.JList; import javax.swing.JPanel; import javax.swing.JPopupMenu; import javax.swing.JScrollPane; import javax.swing.ListSelectionModel; import javax.swing.event.ListDataListener; import javax.swing.event.ListSelectionListener; import java.awt.BorderLayout; import java.awt.Component; import java.awt.Dimension; import java.awt.Rectangle; import java.io.File; /** * Enables clients to create a component for choosing source products. The list of source products can arbitrarily be * composed of * <ul> * <li>currently opened products</li> * <li>single products anywhere in the file system</li> * <li>whole directories anywhere in the file system and</li> * <li>recursive directories anywhere in the file system</li> * </ul> * <p> * The file paths the user chooses are stored as objects of type {@link java.io.File} within the property that is passed * into the constructor. Products that are chosen from the product tree can be retrieved via * {@link #getSourceProducts()}. So, clients of these must take care that the value in the given property is taken into * account as well as the return value of that method. * * The property that serves as target container for the source product paths must be of type * {@code String[].class}. Changes in the list are synchronised with the property. If the changes of the property * values outside this component shall be synchronised with the list, it is necessary that the property lies within a * property container. * * @author thomas */ public class SourceProductList extends ComponentAdapter { private final AppContext appContext; private final InputListModel listModel; private final JList inputPathsList; private String propertyNameLastOpenInputDir; private String propertyNameLastOpenedFormat; private boolean xAxis; private JComponent[] components; private ProductFilter productFilter; private String defaultPattern; /** * Constructor. * * @param appContext The context of the app using this component. * */ public SourceProductList(AppContext appContext) { this.appContext = appContext; this.listModel = new InputListModel(); this.inputPathsList = createInputPathsList(listModel); this.propertyNameLastOpenInputDir = "org.esa.snap.core.ui.product.lastOpenInputDir"; this.propertyNameLastOpenedFormat = "org.esa.snap.core.ui.product.lastOpenedFormat"; this.xAxis = true; this.defaultPattern = null; productFilter = product -> true; } /** * Creates an array of two JPanels. The first panel contains a list displaying the chosen products. The second panel * contains buttons for adding and removing products, laid out in vertical direction. Note that it makes only sense * to use both components. * * @return an array of two JPanels. */ @Override public JComponent[] getComponents() { if (components == null) { components = createComponents(); } return components; } /** * Creates an array of two JPanels. The first panel contains a list displaying the chosen products. The second panel * contains buttons for adding and removing products, laid out in configurable direction. Note that it makes only sense * to use both components. * * @return an array of two JPanels. */ private JComponent[] createComponents() { JPanel listPanel = new JPanel(new BorderLayout()); final JScrollPane scrollPane = new JScrollPane(inputPathsList); scrollPane.setPreferredSize(new Dimension(100, 50)); listPanel.add(scrollPane, BorderLayout.CENTER); final JPanel addRemoveButtonPanel = new JPanel(); int axis = this.xAxis ? BoxLayout.X_AXIS : BoxLayout.Y_AXIS; final BoxLayout buttonLayout = new BoxLayout(addRemoveButtonPanel, axis); addRemoveButtonPanel.setLayout(buttonLayout); addRemoveButtonPanel.add(createAddInputButton()); addRemoveButtonPanel.add(createRemoveInputButton()); JPanel[] panels = new JPanel[2]; panels[0] = listPanel; panels[1] = addRemoveButtonPanel; return panels; } /** * Clears the list of source products. */ public void clear() { listModel.clear(); } /** * Allows clients to add single products. * * @param product A product to add. */ public void addProduct(Product product) { if (product != null) { try { listModel.addElements(product); } catch (ValidationException ve) { Debug.trace(ve); } } } /** * Returns those source products that have been chosen from the product tree. * * @return An array of source products. */ public Product[] getSourceProducts() { return listModel.getSourceProducts(); } private JList<Object> createInputPathsList(InputListModel inputListModel) { JList<Object> list = new JList<>(inputListModel); list.setCellRenderer(new SourceProductListRenderer()); list.setSelectionMode(ListSelectionModel.MULTIPLE_INTERVAL_SELECTION); return list; } /** * If set, users will be asked whether to use this pattern for recursively collecting products from directories. * To unset this, pass {@code null} as pattern. * @since SNAP 3.2 * * @param pattern The pattern to be used when collecting products from directories */ public void setDefaultPattern(String pattern) { this.defaultPattern = pattern; } private AbstractButton createAddInputButton() { final AbstractButton addButton = ToolButtonFactory.createButton(UIUtils.loadImageIcon("icons/Plus24.gif"), false); addButton.addActionListener(e -> { final JPopupMenu popup = new JPopupMenu("Add"); final Rectangle buttonBounds = addButton.getBounds(); final AddProductAction addProductAction = new AddProductAction(appContext, listModel); addProductAction.setProductFilter(productFilter); popup.add(addProductAction); popup.add(new AddFileAction(appContext, listModel, propertyNameLastOpenInputDir, propertyNameLastOpenedFormat)); popup.add(new AddDirectoryAction(appContext, listModel, false, propertyNameLastOpenInputDir, defaultPattern)); popup.add(new AddDirectoryAction(appContext, listModel, true, propertyNameLastOpenInputDir, defaultPattern)); popup.show(addButton, 1, buttonBounds.height + 1); }); return addButton; } private AbstractButton createRemoveInputButton() { final AbstractButton removeButton = ToolButtonFactory.createButton(UIUtils.loadImageIcon("icons/Minus24.gif"), false); removeButton.addActionListener(e -> listModel.removeElementsAt(inputPathsList.getSelectedIndices())); return removeButton; } @Override public void bindComponents() { String propertyName = getBinding().getPropertyName(); Property property = getBinding().getContext().getPropertySet().getProperty(propertyName); Assert.argument(property.getType().equals(String[].class), "property '" + propertyName +"' must be of type String[].class"); listModel.setProperty(property); } @Override public void unbindComponents() { listModel.setProperty(null); } @Override public void adjustComponents() { } /** * Add a listener that is informed every time the list's contents change. * @param changeListener the listener to add */ public void addChangeListener(ListDataListener changeListener) { listModel.addListDataListener(changeListener); } /** * Remove a change listener * @param changeListener the listener to remove */ public void removeChangeListener(ListDataListener changeListener) { listModel.removeListDataListener(changeListener); } /** * Add a listener that is informed every time the list's selection is changed. * @since SNAP 3.2 * @param selectionListener the listener to add */ public void addSelectionListener(ListSelectionListener selectionListener) { inputPathsList.addListSelectionListener(selectionListener); } /** * Remove a selection listener * @since SNAP 3.2 * * @param selectionListener the listener to remove */ public void removeSelectionListener(ListSelectionListener selectionListener) { inputPathsList.removeListSelectionListener(selectionListener); } /** * @since SNAP 3.2 * @param object the object which may be selected or not * @return true, if the object is selected */ public boolean isSelected(Object object) { return inputPathsList.isSelectedIndex(listModel.getIndexOf(object)); } /** * The filter to be used to filter the list of opened products which are offered to the user for selection. * @param productFilter the filter */ public void setProductFilter(ProductFilter productFilter) { this.productFilter = productFilter; } /** * Setter for property name indicating the last directory the user has opened * * @param propertyNameLastOpenedFormat property name indicating the last directory the user has opened */ public void setPropertyNameLastOpenedFormat(String propertyNameLastOpenedFormat) { this.propertyNameLastOpenedFormat = propertyNameLastOpenedFormat; } /** * Setter for property name indicating the last product format the user has opened * @param propertyNameLastOpenInputDir property name indicating the last product format the user has opened */ public void setPropertyNameLastOpenInputDir(String propertyNameLastOpenInputDir) { this.propertyNameLastOpenInputDir = propertyNameLastOpenInputDir; } /** * Setter for xAxis property. * * @param xAxis {@code true} if the buttons on the second panel shall be laid out in horizontal direction */ public void setXAxis(boolean xAxis) { this.xAxis = xAxis; } private static class SourceProductListRenderer extends DefaultListCellRenderer { @Override public Component getListCellRendererComponent(JList list, Object value, int index, boolean isSelected, boolean cellHasFocus) { JLabel label = (JLabel) super.getListCellRendererComponent(list, value, index, isSelected, cellHasFocus); String text; if (value instanceof File) { text = ((File) value).getAbsolutePath(); } else { text = ((ProductNode) value).getDisplayName(); } label.setText(text); label.setToolTipText(text); return label; } } }