/*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;
/**
* 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.
*
* 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.
*
* This dialog is somewhat styled after
* {@link javax.swing.JOptionPane} and uses the message-type constants
* from JOptionPane.
*
* @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 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 selectedItems;
/** 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.
*
* The default sizing, message type, and icon are used. All the
* items are selected by default.
*
* @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);
}
/** 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.
*
* The message type must be one of the message types from
* {@link javax.swing.JOptionPane}. The message type controlls which
* default icon is used.
*
* The default sizing and icon are used.
*
* @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);
}
/** 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.
*
* 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.
*
* @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);
}
/** 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.
*
* 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.
*
* @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(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(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 selectedItems() {
final java.util.List results = new ArrayList();
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 data = new java.util.ArrayList();
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 si = selectedItems();
for (String i : si) {
System.out.println(i);
}
}
};
ld.pack();
ld.setVisible(true);
}
});
}
}