/* * ------------------------------------------------------------------------ * * Copyright (C) 2003 - 2010 * University of Konstanz, Germany and * KNIME GmbH, Konstanz, Germany * Website: http://www.knime.org; Email: contact@knime.org * * This program is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License, Version 3, as * published by the Free Software Foundation. * * 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>. * * Additional permission under GNU GPL version 3 section 7: * * KNIME interoperates with ECLIPSE solely via ECLIPSE's plug-in APIs. * Hence, KNIME and ECLIPSE are both independent programs and are not * derived from each other. Should, however, the interpretation of the * GNU GPL Version 3 ("License") under any applicable laws result in * KNIME and ECLIPSE being a combined program, KNIME GMBH herewith grants * you the additional permission to use and propagate KNIME together with * ECLIPSE with only the license terms in place for ECLIPSE applying to * ECLIPSE and the GNU GPL Version 3 applying for KNIME, provided the * license terms of ECLIPSE themselves allow for the respective use and * propagation of ECLIPSE together with KNIME. * * Additional permission relating to nodes for KNIME that extend the Node * Extension (and in particular that are based on subclasses of NodeModel, * NodeDialog, and NodeView) and that only interoperate with KNIME through * standard APIs ("Nodes"): * Nodes are deemed to be separate and independent programs and to not be * covered works. Notwithstanding anything to the contrary in the * License, the License does not apply to Nodes, you are not required to * license Nodes under the License, and you are granted a license to * prepare and propagate Nodes, in each case even if such Nodes are * propagated with or for interoperation with KNIME. The owner of a Node * may freely choose the license terms applicable to such Node, including * when such Node is propagated with or for interoperation with KNIME. * --------------------------------------------------------------------- * */ package de.mpicbg.knime.knutils.ui; import javax.swing.*; import javax.swing.border.Border; import javax.swing.border.TitledBorder; import javax.swing.event.ChangeEvent; import javax.swing.event.ChangeListener; import java.awt.*; import java.awt.event.ActionEvent; import java.awt.event.ActionListener; import java.awt.event.MouseAdapter; import java.awt.event.MouseEvent; import java.util.*; import java.util.List; /** * Panel is used to select/filter a certain number of columns. * <p/> * <p/> * You can add a property change listener to this class that is notified when the include list changes. * * @author Thomas Gabriel, University of Konstanz */ public class TwoPaneSelectionPanel<Option> extends JPanel { /** * Include list. */ private final JList m_inclList; /** * Include model. */ private final DefaultListModel includeOptions; /** * Exclude list. */ private final JList m_exclList; /** * Exclude model. */ private final DefaultListModel availableOptions; /** * Highlight all search hits in the include model. */ private final JCheckBox m_markAllHitsIncl; /** * Highlight all search hits in the exclude model. */ private final JCheckBox m_markAllHitsExcl; /** * Remove all button. */ private final JButton m_remAllButton; /** * Remove button. */ private final JButton m_remButton; /** * Add all button. */ private final JButton m_addAllButton; /** * Add button. */ private final JButton m_addButton; /** * Search Field in include list. */ private final JTextField m_searchFieldIncl; /** * Search Button for include list. */ private final JButton m_searchButtonIncl; /** * Search Field in exclude list. */ private final JTextField m_searchFieldExcl; /** * Search Button for exclude list. */ private final JButton m_searchButtonExcl; /** * List of DataCellColumnSpecss to keep initial ordering of DataCells. */ private final LinkedHashSet<Option> optionOrder = new LinkedHashSet<Option>(); /** * Border of the include panel, keep it so we can change the title. */ private final TitledBorder m_includeBorder; /** * Border of the include panel, keep it so we can change the title. */ private final TitledBorder m_excludeBorder; private final HashSet<Option> hiddenOptions = new HashSet<Option>(); private List<ChangeListener> m_listeners; /** * Line border for include columns. */ private static final Border INCLUDE_BORDER = BorderFactory.createLineBorder(new Color(0, 221, 0), 2); /** * Line border for exclude columns. */ private static final Border EXCLUDE_BORDER = BorderFactory.createLineBorder(new Color(240, 0, 0), 2); private boolean keepAddOrder = false; /** * Creates a new filter column panel with three component which are the include list, button panel to shift elements * between the two lists, and the exclude list. The include list then will contain all values to filter. * * @param showKeepAllBox true, if an check box to keep all columns is shown * @param cellRenderer */ public TwoPaneSelectionPanel(final boolean showKeepAllBox, ListCellRenderer cellRenderer) { // keeps buttons such add 'add', 'add all', 'remove', and 'remove all' final JPanel buttonPan = new JPanel(); buttonPan.setBorder(BorderFactory.createEmptyBorder(0, 15, 0, 15)); buttonPan.setLayout(new BoxLayout(buttonPan, BoxLayout.Y_AXIS)); buttonPan.add(Box.createVerticalStrut(20)); m_addButton = new JButton("add >>"); m_addButton.setMaximumSize(new Dimension(125, 25)); buttonPan.add(m_addButton); m_addButton.addActionListener(new ActionListener() { public void actionPerformed(final ActionEvent ae) { onAddIt(); } }); buttonPan.add(Box.createVerticalStrut(25)); m_addAllButton = new JButton("add all >>"); m_addAllButton.setMaximumSize(new Dimension(125, 25)); buttonPan.add(m_addAllButton); m_addAllButton.addActionListener(new ActionListener() { public void actionPerformed(final ActionEvent ae) { onAddAll(); } }); buttonPan.add(Box.createVerticalStrut(25)); m_remButton = new JButton("<< remove"); m_remButton.setMaximumSize(new Dimension(125, 25)); buttonPan.add(m_remButton); m_remButton.addActionListener(new ActionListener() { public void actionPerformed(final ActionEvent ae) { onRemIt(); } }); buttonPan.add(Box.createVerticalStrut(25)); m_remAllButton = new JButton("<< remove all"); m_remAllButton.setMaximumSize(new Dimension(125, 25)); buttonPan.add(m_remAllButton); m_remAllButton.addActionListener(new ActionListener() { public void actionPerformed(final ActionEvent ae) { onRemoveAll(); } }); buttonPan.add(Box.createVerticalStrut(20)); buttonPan.add(Box.createGlue()); // include list includeOptions = new DefaultListModel(); m_inclList = new JList(includeOptions); m_inclList.setSelectionMode( ListSelectionModel.MULTIPLE_INTERVAL_SELECTION); m_inclList.addMouseListener(new MouseAdapter() { @Override public void mouseClicked(final MouseEvent me) { if (me.getClickCount() == 2) { onRemIt(); me.consume(); } } }); final JScrollPane jspIncl = new JScrollPane(m_inclList); jspIncl.setMinimumSize(new Dimension(130, 155)); jspIncl.setVerticalScrollBarPolicy(ScrollPaneConstants.VERTICAL_SCROLLBAR_AS_NEEDED); m_searchFieldIncl = new JTextField(8); m_searchButtonIncl = new JButton("Search"); ActionListener actionListenerIncl = new ActionListener() { public void actionPerformed(final ActionEvent e) { onSearch(m_inclList, includeOptions, m_searchFieldIncl, m_markAllHitsIncl.isSelected()); } }; m_searchFieldIncl.addActionListener(actionListenerIncl); m_searchButtonIncl.addActionListener(actionListenerIncl); JPanel inclSearchPanel = new JPanel(new BorderLayout()); inclSearchPanel.add(new JLabel("Column(s): "), BorderLayout.WEST); inclSearchPanel.setBorder(BorderFactory.createEmptyBorder(15, 15, 15, 15)); inclSearchPanel.add(m_searchFieldIncl, BorderLayout.CENTER); inclSearchPanel.add(m_searchButtonIncl, BorderLayout.EAST); m_markAllHitsIncl = new JCheckBox("Select all search hits"); ActionListener actionListenerAllIncl = new ActionListener() { public void actionPerformed(final ActionEvent e) { m_inclList.clearSelection(); onSearch(m_inclList, includeOptions, m_searchFieldIncl, m_markAllHitsIncl.isSelected()); } }; m_markAllHitsIncl.addActionListener(actionListenerAllIncl); inclSearchPanel.add(m_markAllHitsIncl, BorderLayout.PAGE_END); JPanel includePanel = new JPanel(new BorderLayout()); m_includeBorder = BorderFactory.createTitledBorder( INCLUDE_BORDER, " Selection "); includePanel.setBorder(m_includeBorder); includePanel.add(inclSearchPanel, BorderLayout.NORTH); includePanel.add(jspIncl, BorderLayout.CENTER); // exclude list availableOptions = new DefaultListModel(); m_exclList = new JList(availableOptions); m_exclList.setSelectionMode( ListSelectionModel.MULTIPLE_INTERVAL_SELECTION); m_exclList.addMouseListener(new MouseAdapter() { @Override public void mouseClicked(final MouseEvent me) { if (me.getClickCount() == 2) { onAddIt(); me.consume(); } } }); setListCellRenderer(cellRenderer); final JScrollPane jspExcl = new JScrollPane(m_exclList); jspExcl.setMinimumSize(new Dimension(130, 155)); jspExcl.setVerticalScrollBarPolicy(ScrollPaneConstants.VERTICAL_SCROLLBAR_AS_NEEDED); m_searchFieldExcl = new JTextField(8); m_searchButtonExcl = new JButton("Search"); ActionListener actionListenerExcl = new ActionListener() { public void actionPerformed(final ActionEvent e) { onSearch(m_exclList, availableOptions, m_searchFieldExcl, m_markAllHitsExcl.isSelected()); } }; m_searchFieldExcl.addActionListener(actionListenerExcl); m_searchButtonExcl.addActionListener(actionListenerExcl); JPanel exclSearchPanel = new JPanel(new BorderLayout()); exclSearchPanel.setBorder(BorderFactory.createEmptyBorder(15, 15, 15, 15)); exclSearchPanel.add(new JLabel("Column(s): "), BorderLayout.WEST); exclSearchPanel.add(m_searchFieldExcl, BorderLayout.CENTER); exclSearchPanel.add(m_searchButtonExcl, BorderLayout.EAST); m_markAllHitsExcl = new JCheckBox("Select all search hits"); ActionListener actionListenerAllExcl = new ActionListener() { public void actionPerformed(final ActionEvent e) { m_exclList.clearSelection(); onSearch(m_exclList, availableOptions, m_searchFieldExcl, m_markAllHitsExcl.isSelected()); } }; m_markAllHitsExcl.addActionListener(actionListenerAllExcl); exclSearchPanel.add(m_markAllHitsExcl, BorderLayout.PAGE_END); JPanel excludePanel = new JPanel(new BorderLayout()); m_excludeBorder = BorderFactory.createTitledBorder( EXCLUDE_BORDER, " Options "); excludePanel.setBorder(m_excludeBorder); excludePanel.add(exclSearchPanel, BorderLayout.NORTH); excludePanel.add(jspExcl, BorderLayout.CENTER); JPanel buttonPan2 = new JPanel(new GridLayout()); Border border = BorderFactory.createTitledBorder(" "); buttonPan2.setBorder(border); buttonPan2.add(buttonPan); // adds include, button, exclude component JPanel center = new JPanel(); center.setLayout(new BoxLayout(center, BoxLayout.X_AXIS)); center.add(excludePanel); center.add(buttonPan2); center.add(includePanel); JPanel all = new JPanel(); all.setLayout(new BoxLayout(all, BoxLayout.Y_AXIS)); all.add(center); super.setLayout(new GridLayout(1, 1)); super.add(all); } /** * Enables or disables all components on this panel. {@inheritDoc} */ @Override public void setEnabled(final boolean enabled) { super.setEnabled(enabled); enabledComponents(enabled); } private void enabledComponents(final boolean newEnabled) { m_searchFieldIncl.setEnabled(newEnabled); m_searchButtonIncl.setEnabled(newEnabled); m_searchFieldExcl.setEnabled(newEnabled); m_searchButtonExcl.setEnabled(newEnabled); m_inclList.setEnabled(newEnabled); m_exclList.setEnabled(newEnabled); m_markAllHitsIncl.setEnabled(newEnabled); m_markAllHitsExcl.setEnabled(newEnabled); m_remAllButton.setEnabled(newEnabled); m_remButton.setEnabled(newEnabled); m_addAllButton.setEnabled(newEnabled); m_addButton.setEnabled(newEnabled); } /** * Adds a listener which gets informed whenever the column filtering changes. * * @param listener the listener */ public void addChangeListener(final ChangeListener listener) { if (m_listeners == null) { m_listeners = new ArrayList<ChangeListener>(); } m_listeners.add(listener); } /** * Removes the given listener from this filter column panel. * * @param listener the listener. */ public void removeChangeListener(final ChangeListener listener) { if (m_listeners != null) { m_listeners.remove(listener); } } /** * Removes all column filter change listener. */ public void removeAllColumnFilterChangeListener() { if (m_listeners != null) { m_listeners.clear(); } } private void fireFilteringChangedEvent() { if (m_listeners != null) { for (ChangeListener listener : m_listeners) { listener.stateChanged(new ChangeEvent(this)); } } } /** * Called by the 'remove >>' button to exclude the selected elements from the include list. */ private void onRemIt() { // add all selected elements from the include to the exclude list Object[] o = m_inclList.getSelectedValues(); HashSet<Object> hash = new HashSet<Object>(); hash.addAll(Arrays.asList(o)); for (Enumeration<?> e = availableOptions.elements(); e.hasMoreElements();) { hash.add(e.nextElement()); } boolean changed = false; for (Object anO : o) { changed |= includeOptions.removeElement(anO); } availableOptions.removeAllElements(); for (Option c : optionOrder) { if (hash.contains(c)) { availableOptions.addElement(c); } } if (changed) { fireFilteringChangedEvent(); } } /** * Called by the 'remove >>' button to exclude all elements from the include list. */ private void onRemoveAll() { boolean changed = includeOptions.elements().hasMoreElements(); includeOptions.removeAllElements(); availableOptions.removeAllElements(); for (Option c : optionOrder) { if (!hiddenOptions.contains(c)) { availableOptions.addElement(c); } } if (changed) { fireFilteringChangedEvent(); } } /** * Called by the '<< add' button to include the selected elements from the exclude list. */ private void onAddIt() { // add all selected elements from the exclude to the include list Object[] o = m_exclList.getSelectedValues(); Collection<Object> hash = keepAddOrder ? new HashSet<Object>() : new ArrayList<Object>(); for (Enumeration<?> e = includeOptions.elements(); e.hasMoreElements();) { hash.add(e.nextElement()); } hash.addAll(Arrays.asList(o)); boolean changed = false; includeOptions.removeAllElements(); if (keepAddOrder) { for (int i = 0; i < o.length; i++) { changed |= availableOptions.removeElement(o[i]); } for (Option c : optionOrder) { if (hash.contains(c)) { includeOptions.addElement(c); } } } else { for (Object o1 : hash) { changed |= availableOptions.removeElement(o1); includeOptions.addElement(o1); } } if (changed) { fireFilteringChangedEvent(); } } /** * Called by the '<< add all' button to include all elements from the exclude list. */ private void onAddAll() { boolean changed = availableOptions.elements().hasMoreElements(); includeOptions.removeAllElements(); availableOptions.removeAllElements(); for (Option c : optionOrder) { if (!hiddenOptions.contains(c)) { includeOptions.addElement(c); } } if (changed) { fireFilteringChangedEvent(); } } /** * Updates this filter panel by removing all current selections from the include and exclude list. The include list * will contains all column names from the spec afterwards. * * @param options the spec to retrieve the option-names from */ public void update(List<Option> options, List<Option> includes) { assert (options != null); optionOrder.clear(); includeOptions.removeAllElements(); availableOptions.removeAllElements(); hiddenOptions.clear(); for (Option option : options) { optionOrder.add(option); availableOptions.addElement(option); } include(includes); repaint(); } public void include(List<Option> includes) { for (Option include : includes) { if (availableOptions.contains(include)) { availableOptions.removeElement(include); } includeOptions.addElement(include); } } /** * Returns all columns from the exclude list. * * @return a set of all columns from the exclude list */ public List<Option> getExcludedColumnSet() { return getColumnList(availableOptions); } /** * Returns all columns from the include list. * * @return a list of all columns from the include list */ public List<Option> getIncludedColumnSet() { return getColumnList(includeOptions); } /** * Helper for the get***ColumnList methods. * * @param model The list from which to retrieve the elements */ private List<Option> getColumnList(final ListModel model) { final List<Option> list = new ArrayList<Option>(); for (int i = 0; i < model.getSize(); i++) { Object o = model.getElementAt(i); list.add((Option) o); } return list; } /** * This method is called when the user wants to search the given {@link JList} for the text of the given {@link * JTextField}. * * @param list the list to search in * @param model the list model on which the list is based on * @param searchField the text field with the text to search for * @param markAllHits if set to <code>true</code> the method will mark all occurrences of the given search text in * the given list. If set to <code>false</code> the method will mark the next occurrences of the * search text after the current marked list element. */ private static void onSearch(final JList list, final DefaultListModel model, final JTextField searchField, final boolean markAllHits) { if (list == null || model == null || searchField == null) { return; } final String searchStr = searchField.getText().trim(); if (model.isEmpty() || searchStr.equals("")) { list.clearSelection(); return; } if (markAllHits) { int[] searchHits = getAllSearchHits(list, searchStr); list.clearSelection(); if (searchHits.length > 0) { list.setSelectedIndices(searchHits); list.scrollRectToVisible(list.getCellBounds(searchHits[0], searchHits[0])); } } else { int start = Math.max(0, list.getSelectedIndex() + 1); if (start >= model.getSize()) { start = 0; } int f = searchInList(list, searchStr, start); if (f >= 0) { list.scrollRectToVisible(list.getCellBounds(f, f)); list.setSelectedIndex(f); } } } /* * Finds in the list any occurrence of the argument string (as substring). */ private static int searchInList(final JList list, final String str, final int startIndex) { // this method was (slightly modified) copied from // JList#getNextMatch ListModel model = list.getModel(); int max = model.getSize(); String prefix = str; if (prefix == null) { throw new IllegalArgumentException(); } if (startIndex < 0 || startIndex >= max) { throw new IllegalArgumentException(); } prefix = prefix.toUpperCase(); int index = startIndex; do { Object o = model.getElementAt(index); if (o != null) { String string; if (o instanceof String) { string = ((String) o).toUpperCase(); } else { string = o.toString(); if (string != null) { string = string.toUpperCase(); } } if (string != null && string.indexOf(prefix) >= 0) { return index; } } index = (index + 1 + max) % max; } while (index != startIndex); return -1; } /** * Uses the {@link #searchInList(JList, String, int)} method to get all occurrences of the given string in the given * list and returns the index off all occurrences as a <code>int[]</code>. * * @param list the list to search in * @param str the string to search for * @return <code>int[]</code> with the indices off all objects from the given list which match the given string. If * no hits exists the method returns an empty <code>int[]</code>. * @see #searchInList(JList, String, int) */ private static int[] getAllSearchHits(final JList list, final String str) { ListModel model = list.getModel(); int max = model.getSize(); final ArrayList<Integer> hits = new ArrayList<Integer>(max); int index = 0; do { int tempIndex = searchInList(list, str, index); // if the search returns no hit or returns a hit before the // current search position exit the while loop if (tempIndex < index || tempIndex < 0) { break; } index = tempIndex; hits.add(new Integer(index)); // increase the index to start the search from the next position // after the current hit index++; } while (index < max); if (hits.size() > 0) { final int[] resultArray = new int[hits.size()]; for (int i = 0, length = hits.size(); i < length; i++) { resultArray[i] = hits.get(i).intValue(); } return resultArray; } return new int[0]; } /** * Set the renderer that is used for both list in this panel. * * @param renderer the new renderer being used * @see JList#setCellRenderer(javax.swing.ListCellRenderer) */ protected final void setListCellRenderer(final ListCellRenderer renderer) { m_inclList.setCellRenderer(renderer); m_exclList.setCellRenderer(renderer); } // /** * Removes the given columns form either include or exclude list and notifies all listeners. Does not throw an * exception if the argument contains <code>null</code> elements or is not contained in any of the lists. * * @param options the columns to remove */ public final void hideColumns(final Option... options) { boolean changed = false; for (Option column : options) { if (includeOptions.contains(column)) { hiddenOptions.add(column); changed |= includeOptions.removeElement(column); } else if (availableOptions.contains(column)) { hiddenOptions.add(column); changed |= availableOptions.removeElement(column); } } if (changed) { fireFilteringChangedEvent(); } } /** * Re-adds all remove/hidden columns to the exclude list. */ public final void resetHiding() { if (hiddenOptions.isEmpty()) { return; } // add all selected elements from the include to the exclude list HashSet<Object> hash = new HashSet<Object>(); hash.addAll(hiddenOptions); for (Enumeration<?> e = availableOptions.elements(); e.hasMoreElements();) { hash.add(e.nextElement()); } availableOptions.removeAllElements(); for (Option c : optionOrder) { if (hash.contains(c)) { availableOptions.addElement(c); } } hiddenOptions.clear(); } /** * Sets the title of the include panel. * * @param title the new title */ public final void setIncludeTitle(final String title) { m_includeBorder.setTitle(title); } /** * Sets the title of the exclude panel. * * @param title the new title */ public final void setExcludeTitle(final String title) { m_excludeBorder.setTitle(title); } /** * Setter for the original "Remove All" button. * * @param text the new button title */ public void setRemoveAllButtonText(final String text) { m_remAllButton.setText(text); } /** * Setter for the original "Add All" button. * * @param text the new button title */ public void setAddAllButtonText(final String text) { m_addAllButton.setText(text); } /** * Setter for the original "Remove" button. * * @param text the new button title */ public void setRemoveButtonText(final String text) { m_remButton.setText(text); } /** * Setter for the original "Add" button. * * @param text the new button title */ public void setAddButtonText(final String text) { m_addButton.setText(text); } }