A popup dialog with a message and a scrollable list of items
/*BEGIN_COPYRIGHT_BLOCK * * Copyright (c) 2001-2008, JavaPLT group at Rice University (drjava@rice.edu) * All rights reserved. * * Redistribution and use in source and binary forms, with or without * modification, are permitted provided that the following conditions are met: * * Redistributions of source code must retain the above copyright * notice, this list of conditions and the following disclaimer. * * Redistributions in binary form must reproduce the above copyright * notice, this list of conditions and the following disclaimer in the * documentation and/or other materials provided with the distribution. * * Neither the names of DrJava, the JavaPLT group, Rice University, nor the * names of its contributors may be used to endorse or promote products * derived from this software without specific prior written permission. * * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, * EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, * PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR * PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF * LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING * NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. * * This software is Open Source Initiative approved Open Source Software. * Open Source Initative Approved is a trademark of the Open Source Initiative. * * This file is part of DrJava. Download the current version of this project * from http://www.drjava.org/ or http://sourceforge.net/projects/drjava/ * * END_COPYRIGHT_BLOCK*/ import javax.swing.*; import java.awt.*; import java.awt.event.*; import java.util.*; import javax.swing.table.AbstractTableModel; /** * <p>The ScrollableListSelectionDialog is a popup dialog with a message * and a scrollable list of items. Each item may be either selected or * unselected. A ScrollableListSelectionDialog should be used when * an operation needs to act on a variable number of items, for * example, when saving modified files.</p> * * <p>The message (also know as the leader text) is displayed above the * items with an optional icon. The items are displayed in a scrollable * table. A column of checkboxes allows selection of the items. Buttons * are added below the list of items.</p> * * <p>This dialog is somewhat styled after * {@link javax.swing.JOptionPane} and uses the message-type constants * from JOptionPane.</p> * * @author Chris Warrington * @version $Id$ * @since 2007-04-08 */ public class ScrollableListSelectionDialog extends JDialog { /** A enumeration of the various selection states. */ public enum SelectionState { /** Indicates that an item is selected. */ SELECTED, /** Indicates that an item is not selected. */ UNSELECTED }; /** The default width for this dialog. */ private static final int DEFAULT_WIDTH = 400; /** The default height for this dialog. */ private static final int DEFAULT_HEIGHT = 450; /** The ratio of the screen width to use by default. */ private static final double WIDTH_RATIO = .75; /** The ratio of the screen height to use by default. */ private static final double HEIGHT_RATIO = .50; /** The table displaying the items. */ protected final JTable table; /** The AbstractTableModel backing the table. */ protected final AbstractTableModel tableModel; /** The number of columns in the table. */ private static final int NUM_COLUMNS = 2; /** The column index of the checkboxes column. */ private static final int CHECKBOXES_COLUMN_INDEX = 0; /** The column index of the strings column. */ private static final int STRINGS_COLUMN_INDEX = 1; /** The items in the table. */ protected final Vector<String> dataAsStrings; /** The selected items in the table. This Vector maps to * _dataAsStrings by index. This value may be accessed by multiple * threads. Threads wishing to access it should acquire its * intrinsic lock. */ protected final Vector<Boolean> selectedItems; /** <p>Creates a new ScrollableListSelectionDialog with the given * title, leader text, and items. The list of items is used to * construct an internal string list that is not backed by the original * list. Changes made to the list or items after dialog construction * will not be reflected in the dialog.</p> * * <p>The default sizing, message type, and icon are used. All the * items are selected by default.</p> * * @param owner The frame that owns this dialog. May be {@code null}. * @param dialogTitle The text to use as the dialog title. * @param leaderText Text to display before the list of items. * @param listItems The items to display in the list. * @param itemDescription A textual description of the items. This is used as the column heading for the items. * * @throws IllegalArgumentException if {@code listItems} is {@code null.} */ public ScrollableListSelectionDialog(final Frame owner, final String dialogTitle, final String leaderText, final Collection<?> listItems, final String itemDescription) { this(owner, dialogTitle, leaderText, listItems, itemDescription, SelectionState.SELECTED, JOptionPane.PLAIN_MESSAGE); } /** <p>Creates a new ScrollableListSelectionDialog with the given * title, leader text, items, and message type. The list of items is * used to construct an internal string list that is not backed by the * original list. Changes made to the list or items after dialog * construction will not be reflected in the dialog.</p> * * <p>The message type must be one of the message types from * {@link javax.swing.JOptionPane}. The message type controlls which * default icon is used.</p> * * <p>The default sizing and icon are used.</p> * * @param owner The frame that owns this dialog. May be {@code null}. * @param dialogTitle The text to use as the dialog title. * @param leaderText Text to display before the list of items. * @param listItems The items to display in the list. * @param itemDescription A textual description of the items. This is used as the column heading for the items. * @param defaultSelection The default selection state (selected or unselected) for the items. * @param messageType The type of dialog message. * * @throws IllegalArgumentException if {@code listItems} is {@code null.} * @throws IllegalArgumentException if the message type is unknown or {@code listItems} is {@code null.} */ public ScrollableListSelectionDialog(final Frame owner, final String dialogTitle, final String leaderText, final Collection<?> listItems, final String itemDescription, final SelectionState defaultSelection, final int messageType) { this(owner, dialogTitle, leaderText, listItems, itemDescription, defaultSelection, messageType, DEFAULT_WIDTH, DEFAULT_HEIGHT, null, true); } /** <p>Creates a new ScrollableListSelectionDialog with the given * title, leader text, items, message type, width, height, and icon. * The list of items is used to construct an internal string list that * is not backed by the original list. Changes made to the list or * items after dialog construction will not be reflected in the * dialog.</p> * * <p>The message type must be one of the message types from * {@link javax.swing.JOptionPane}. The message type controlls which * default icon is used. If {@code icon} is non-null, it is used * instead of the default icon.</p> * * @param owner The frame that owns this dialog. May be {@code null}. * @param dialogTitle The text to use as the dialog title. * @param leaderText Text to display before the list of items. * @param listItems The items to display in the list. * @param itemDescription A textual description of the items. This is used as the column heading for the items. * @param defaultSelection The default selection state (selected or unselected) for the items. * @param messageType The type of dialog message. * @param width The width of the dialog box. * @param height The height of the dialog box. * @param icon The icon to display. May be {@code null}. * * @throws IllegalArgumentException if {@code listItems} is {@code null.} * @throws IllegalArgumentException if the message type is unknown or {@code listItems} is {@code null.} */ public ScrollableListSelectionDialog(final Frame owner, final String dialogTitle, final String leaderText, final Collection<?> listItems, final String itemDescription, final SelectionState defaultSelection, final int messageType, final int width, final int height, final Icon icon) { this(owner, dialogTitle, leaderText, listItems, itemDescription, defaultSelection, messageType, width, height, icon, false); } /** <p>Creates a new ScrollableListSelectionDialog with the given * title, leader text, items, message type, width, height, and icon. * The list of items is used to construct an internal string list that * is not backed by the original list. Changes made to the list or * items after dialog construction will not be reflected in the * dialog.</p> * * <p>The message type must be one of the message types from * {@link javax.swing.JOptionPane}. The message type controlls which * default icon is used. If {@code icon} is non-null, it is used * instead of the default icon.</p> * * @param owner The frame that owns this dialog. May be {@code null}. * @param dialogTitle The text to use as the dialog title. * @param leaderText Text to display before the list of items. * @param listItems The items to display in the list. * @param itemDescription A textual description of the items. This is used as the column heading for the items. * @param defaultSelection The default selection state (selected or unselected) for the items. * @param messageType The type of dialog message. * @param width The width of the dialog box. * @param height The height of the dialog box. * @param icon The icon to display. May be {@code null}. * @param fitToScreen If {@code true}, the width and height of the dialog will be calculated using the screen * dimensions, {@link #WIDTH_RATIO}, and {@link #HEIGHT_RATIO}. If {@code false}, the provided width and * height will be used. * @throws IllegalArgumentException if {@code listItems} is {@code null.} * @throws IllegalArgumentException if the message type is unknown or {@code listItems} is {@code null.} */ private ScrollableListSelectionDialog(final Frame owner, final String dialogTitle, final String leaderText, final Collection<?> listItems, final String itemDescription, final SelectionState defaultSelection, final int messageType, final int width, final int height, final Icon icon, final boolean fitToScreen) { super(owner, dialogTitle, true); if (!_isknownMessageType(messageType)) { throw new IllegalArgumentException("The message type \"" + messageType + "\" is unknown"); } if (listItems == null) { throw new IllegalArgumentException("listItems cannot be null"); } /* create the leader text panel */ JLabel dialogIconLabel = null; if (icon != null) { //use the user-provided icon dialogIconLabel = new JLabel(icon); } else { //lookup the message-dependent icon Icon messageIcon = _getIcon(messageType); if (messageIcon != null) { dialogIconLabel = new JLabel(messageIcon); } } final JPanel leaderPanel = new JPanel(); final JLabel leaderLabel = new JLabel(leaderText); leaderPanel.setLayout(new FlowLayout(FlowLayout.LEFT)); if (dialogIconLabel != null) { leaderPanel.add(dialogIconLabel); } leaderPanel.add(leaderLabel); /* create the table */ //copy the items string representations into a vector dataAsStrings = new Vector<String>(listItems.size()); for (Object obj : listItems) { if (obj != null) { final String objAsString = obj.toString(); dataAsStrings.add(objAsString); } } dataAsStrings.trimToSize(); final int numItems = dataAsStrings.size(); selectedItems = new Vector<Boolean>(numItems); synchronized(selectedItems) { for (int i = 0; i < numItems; ++i) { selectedItems.add(i, defaultSelection == SelectionState.SELECTED); } selectedItems.trimToSize(); } assert selectedItems.size() == dataAsStrings.size(); tableModel = new AbstractTableModel() { //@Override - uncomment when we start compiling with Java 6 public int getRowCount() { return numItems; } //@Override - uncomment when we start compiling with Java 6 public int getColumnCount() { return NUM_COLUMNS; } //@Override - uncomment when we start compiling with Java 6 public Object getValueAt(int row, int column) { if (column == CHECKBOXES_COLUMN_INDEX) { assert row >= 0; assert row < numItems; synchronized(selectedItems) { return selectedItems.get(row); } } else if (column == STRINGS_COLUMN_INDEX) { assert row >= 0; assert row < numItems; return dataAsStrings.get(row); } else { assert false; return null; } } @Override public String getColumnName(int column) { if (column == CHECKBOXES_COLUMN_INDEX) { return ""; } else if (column == STRINGS_COLUMN_INDEX) { return itemDescription; } else { assert false; return ""; } } @Override public Class<?> getColumnClass(final int columnIndex) { if (columnIndex == CHECKBOXES_COLUMN_INDEX) { return Boolean.class; } else if (columnIndex == STRINGS_COLUMN_INDEX) { return String.class; } else { assert false; return Object.class; } } @Override public boolean isCellEditable(final int rowIndex, final int columnIndex) { return columnIndex == CHECKBOXES_COLUMN_INDEX; //only checkboxes are editable } @Override public void setValueAt(final Object newValue, final int rowIndex, final int columnIndex) { assert columnIndex == CHECKBOXES_COLUMN_INDEX; assert rowIndex >= 0; assert rowIndex < numItems; assert newValue instanceof Boolean; final Boolean booleanValue = (Boolean)newValue; synchronized(selectedItems) { selectedItems.set(rowIndex, booleanValue); } } }; table = new JTable(tableModel); /* * this listener enabled clicking in the string column to update the * checkbox. */ table.addMouseListener(new MouseAdapter() { @Override public void mouseClicked(final MouseEvent e) { final Point clickPoint = e.getPoint(); // which column was clicked on final int clickColumn = table.columnAtPoint(clickPoint); if (clickColumn == STRINGS_COLUMN_INDEX) { //it was the strings column, so update the check status of the row //Swing does not do this automatically final int clickRow = table.rowAtPoint(clickPoint); if (clickRow >= 0 && clickRow < numItems) { synchronized(selectedItems) { final boolean currentValue = selectedItems.get(clickRow); final boolean newValue = !currentValue; selectedItems.set(clickRow, newValue); /* We are deliberately holding on to the lock while the * listeners are notified. This, in theory, speeds up the * listeners because they don't have to re-acquire the * lock. Because the internals of Swing are unknown, the * lock may need to be released before the listeners are * notified. Only time will tell. * * PS: If it turns out that holding the lock during * the listener updates is a problem, modify this comment * accordingly. Thank you. */ tableModel.fireTableCellUpdated(clickRow, CHECKBOXES_COLUMN_INDEX); } } } } }); //set the column sizes table.getColumnModel().getColumn(CHECKBOXES_COLUMN_INDEX).setMinWidth(15); table.getColumnModel().getColumn(CHECKBOXES_COLUMN_INDEX).setMaxWidth(30); table.getColumnModel().getColumn(CHECKBOXES_COLUMN_INDEX).setPreferredWidth(20); table.getColumnModel().getColumn(CHECKBOXES_COLUMN_INDEX).sizeWidthToFit(); //create a scrollable view around the table final JScrollPane scrollPane = new JScrollPane(table); /* create the select all/select none panel */ final JPanel selectButtonsPanel = new JPanel(); selectButtonsPanel.setLayout(new FlowLayout(FlowLayout.CENTER)); _addSelectButtons(selectButtonsPanel); /* create the button panel */ final JPanel buttonPanel = new JPanel(); buttonPanel.setLayout(new FlowLayout(FlowLayout.CENTER)); //allow children to add additional buttons, if overridden _addButtons(buttonPanel); /* create the center panel which contains the scroll pane and the * select all/select none buttons */ final JPanel centerPanel = new JPanel(); centerPanel.setLayout(new BorderLayout()); centerPanel.add(selectButtonsPanel, BorderLayout.NORTH); centerPanel.add(scrollPane, BorderLayout.CENTER); /* create the dialog */ final JPanel contentPanel = new JPanel(); contentPanel.setLayout(new BorderLayout(10, 5)); contentPanel.setBorder(BorderFactory.createEmptyBorder(5, 10, 0, 10)); contentPanel.add(leaderPanel, BorderLayout.NORTH); contentPanel.add(centerPanel, BorderLayout.CENTER); contentPanel.add(buttonPanel, BorderLayout.SOUTH); getContentPane().add(contentPanel); /* calculate the dialog's dimensions */ final Dimension dialogSize = new Dimension(); if (fitToScreen) { //use the screen dimensions to calculate the dialog's final Dimension screenSize = Toolkit.getDefaultToolkit().getScreenSize(); int screenBasedWidth = (int) (WIDTH_RATIO * screenSize.getWidth()); int screenBasedHeight = (int) (HEIGHT_RATIO * screenSize.getHeight()); dialogSize.setSize(Math.max(DEFAULT_WIDTH, screenBasedWidth), Math.max(DEFAULT_HEIGHT, screenBasedHeight)); } else { //use the user-provided dimensions dialogSize.setSize(width, height); } setSize(dialogSize); } /** A method to check if they given message type is a know message * type. * * @param messageType The message type to check * @return {@code true} if the message type is known, {@code false} otherwise */ private boolean _isknownMessageType(final int messageType) { return messageType == JOptionPane.ERROR_MESSAGE || messageType == JOptionPane.INFORMATION_MESSAGE || messageType == JOptionPane.WARNING_MESSAGE || messageType == JOptionPane.QUESTION_MESSAGE || messageType == JOptionPane.PLAIN_MESSAGE; } /** Lookup the icon associated with the given messageType. The message * type must be one of the message types from * {@link javax.swing.JOptionPane}. * * @param messageType The message for which the icon is requested. * @return The message's icon or {@code null} is no icon was found. */ private Icon _getIcon(final int messageType) { assert _isknownMessageType(messageType); /* The OptionPane.xxxIcon constants were taken from * javax.swing.plaf.basic.BasicOptionPaneUI, which may changed * without notice. */ if (messageType == JOptionPane.ERROR_MESSAGE) { return UIManager.getIcon("OptionPane.errorIcon"); } else if (messageType == JOptionPane.INFORMATION_MESSAGE) { return UIManager.getIcon("OptionPane.informationIcon"); } else if (messageType == JOptionPane.WARNING_MESSAGE) { return UIManager.getIcon("OptionPane.warningIcon"); } else if (messageType == JOptionPane.QUESTION_MESSAGE) { return UIManager.getIcon("OptionPane.questionIcon"); } else if (messageType == JOptionPane.PLAIN_MESSAGE) { return null; } else { //should never get here assert false; } return null; } /** Adds the "Select All" and "Select None" buttons * to the given panel. * * @param selectButtonsPanel The panel that should contain the buttons. */ private void _addSelectButtons(final JPanel selectButtonsPanel) { final JButton selectAllButton = new JButton("Select All"); selectAllButton.addActionListener(new SelectAllNoneActionListener(SelectionState.SELECTED)); selectButtonsPanel.add(selectAllButton); final JButton selectNoneButton = new JButton("Select None"); selectNoneButton.addActionListener(new SelectAllNoneActionListener(SelectionState.UNSELECTED)); selectButtonsPanel.add(selectNoneButton); } /** Adds buttons to the bottom of the dialog. By default, a single * "OK" button is added that calls {@link #closeDialog}. It * is also set as the dialog's default button. * * Inheritors should feel free the change settings of the panel such * as the layout manager. However, no guarantees are made that every * change will work with every version of this class. * * @param buttonPanel The JPanel that should contain the buttons. */ protected void _addButtons(final JPanel buttonPanel) { final JButton okButton = new JButton("OK"); okButton.addActionListener(new ActionListener() { public void actionPerformed(ActionEvent notUsed) { closeDialog(); } }); buttonPanel.add(okButton); getRootPane().setDefaultButton(okButton); } /** * Shows the dialog. */ public void showDialog() { pack(); setVisible(true); } /** Should be called when the dialog should be closed. The default implementation * simply hides the dialog. */ protected void closeDialog() { setVisible(false); } /** Returns the string representation of those items that are * currently selected. The items will be in the same relative order * as they were at construction time. The resultant collection may be * empty. The resultant collection is unmodifiable. The resultant * collection is simply a snapshot (i.e., It will not be updated as * more items are selected.). This method may be called from * non-event queue threads. * * @return The currently selected items. */ public java.util.List<String> selectedItems() { final java.util.List<String> results = new ArrayList<String>(); synchronized(selectedItems) { /* This entire loop is synchronized so that we get a consistent * view of the selected items. It is also faster. */ for (int i = 0; i < dataAsStrings.size(); ++i) { if (selectedItems.get(i)) { results.add(dataAsStrings.get(i)); } } } return Collections.unmodifiableList(results); } /** An ActionListener that handles the "Select All" and * "Select None" buttons. It will set the selection state * of every item to the given selection state. */ private class SelectAllNoneActionListener implements ActionListener { /** The value that the selection state will be set to when this * listener runs. */ private final boolean _setToValue; /** * Creates a new SelectAllNoneActionListener that will set the state * of every item to the given state. * * @param setToState The state to set all the items to. */ public SelectAllNoneActionListener(SelectionState setToState) { _setToValue = setToState == SelectionState.SELECTED; } /** * The code that runs in response to the button's action. * This is the code that actually sets the selection state of the * items. * * @param notUsed Not used. */ public void actionPerformed(ActionEvent notUsed) { /* See comment in the table's mouse listener for a discussion * about the duration of the lock. */ synchronized(selectedItems) { for (int i = 0; i < selectedItems.size(); ++i) { selectedItems.set(i, _setToValue); } tableModel.fireTableRowsUpdated(0, Math.max(0, selectedItems.size() - 1)); } } } /** A simple main method for testing purposes. * * @param args Not used. */ public static void main(String args[]) { final Collection<String> data = new java.util.ArrayList<String>(); data.add("how"); data.add("now"); data.add("brown"); data.add("cow"); EventQueue.invokeLater(new Runnable() { public void run() { ScrollableListSelectionDialog ld = new ScrollableListSelectionDialog(null, "TITLE", "LEADER", data, "Words", SelectionState.SELECTED, JOptionPane.ERROR_MESSAGE) { @Override protected void closeDialog() { super.closeDialog(); Collection<String> si = selectedItems(); for (String i : si) { System.out.println(i); } } }; ld.pack(); ld.setVisible(true); } }); } }