Swing Components Java

/*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);
      }
    });
  }
}