/*
* @(#)CheckBoxList.java 4/21/2005
*
* Copyright 2002 - 2005 JIDE Software Inc. All rights reserved.
*/
package com.jidesoft.swing;
import javax.swing.*;
import javax.swing.text.Position;
import java.awt.*;
import java.awt.event.*;
import java.util.HashMap;
import java.util.Map;
import java.util.Vector;
/**
* <code>CheckBoxListWithSelectable</code> is a special JList which uses JCheckBox as the list element. In addition to
* regular JList feature, it also allows you select any number of elements in the list by selecting the check boxes.
* <p/>
* The element is ListModel should be an instance of {@link Selectable}. If you have your own class that represents the
* element in the list, you can implement <code>Selectable</code> and implements a few very simple methods. If your
* elements are already in an array or Vector that you pass in to the constructor of JList, we will convert them to
* {@link DefaultSelectable} which implements <code>Selectable</code> interface.
* <p/>
* To select an element, user can mouse click on the check box, or highlight the rows and press SPACE key to toggle the
* selections.
* <p/>
* To listen to the check box selection change, you can call addItemListener to add an ItemListener.
* <p/>
* Please note, there are two implementations of CheckBoxList. CheckBoxListWithSelectable is one. There is also another
* one call CheckBoxList. CheckBoxListWithSelectable is actually the old implementation. In 1.9.2, we introduced a new
* implementation and renamed the old implementation to CheckBoxListWithSelectable. The main difference between the two
* implementation is at how the selection state is kept. In new implementation, the selection state is kept at a
* separate ListSelectionModel which you can get using {@link CheckBoxList#getCheckBoxListSelectionModel()}. The old
* implementation kept the selection state at Selectable object in the ListModel.
*/
public class CheckBoxListWithSelectable extends JList implements ItemSelectable {
protected CheckBoxListCellRenderer _listCellRenderer;
public static final String PROPERTY_CHECKBOX_ENABLED = "checkBoxEnabled";
public static final String PROPERTY_CLICK_IN_CHECKBOX_ONLY = "clickInCheckBoxOnly";
private boolean _checkBoxEnabled = true;
private boolean _clickInCheckBoxOnly = true;
/**
* Constructs a <code>CheckBoxList</code> with an empty model.
*/
public CheckBoxListWithSelectable() {
init();
}
/**
* Constructs a <code>CheckBoxList</code> that displays the elements in the specified <code>Vector</code>. If the
* Vector contains elements which is not an instance of {@link Selectable}, it will wrap it automatically into
* {@link DefaultSelectable} and add to ListModel.
*
* @param listData the <code>Vector</code> to be loaded into the data model
*/
public CheckBoxListWithSelectable(final Vector<?> listData) {
super(wrap(listData));
init();
}
/**
* Constructs a <code>CheckBoxList</code> that displays the elements in the specified <code>Object[]</code>. If the
* Object array contains elements which is not an instance of {@link Selectable}, it will wrap it automatically into
* {@link DefaultSelectable} and add to ListModel.
*
* @param listData the array of Objects to be loaded into the data model
*/
public CheckBoxListWithSelectable(final Object[] listData) {
super(wrap(listData));
init();
}
/**
* Constructs a <code>CheckBoxList</code> that displays the elements in the specified, non-<code>null</code> model.
* All <code>CheckBoxList</code> constructors delegate to this one.
* <p/>
* Please note, if you are using this constructor, please make sure all elements in dataModel are instance of {@link
* Selectable}.
*
* @param dataModel the data model for this list
* @throws IllegalArgumentException if <code>dataModel</code> is <code>null</code>
*/
public CheckBoxListWithSelectable(ListModel dataModel) {
super(wrap(dataModel));
init();
}
/**
* Initialize the CheckBoxList.
*/
protected void init() {
setSelectionMode(ListSelectionModel.MULTIPLE_INTERVAL_SELECTION);
_listCellRenderer = createCellRenderer();
Handler handle = createHandler();
addMouseListener(handle);
addKeyListener(handle);
}
/**
* Creates the cell renderer.
*
* @return the cell renderer.
*/
protected CheckBoxListCellRenderer createCellRenderer() {
return new CheckBoxListCellRenderer();
}
/**
* Creates the mouse listener and key listener used by CheckBoxList.
*
* @return the Handler.
*/
protected Handler createHandler() {
return new Handler(this);
}
/**
* Sets the selected elements.
*
* @param elements the elements to be selected
*/
public void setSelectedObjects(Object[] elements) {
Map<Object, String> selected = new HashMap<Object, String>();
for (Object element : elements) {
selected.put(element, "");
}
setSelectedObjects(selected);
}
/**
* Sets the selected objects.
*
* @param objects the elements to be selected in a Vector.
*/
public void setSelectedObjects(Vector<?> objects) {
Map<Object, String> selected = new HashMap<Object, String>();
for (Object element : objects) {
selected.put(element, "");
}
setSelectedObjects(selected);
}
@Override
public ListCellRenderer getCellRenderer() {
if (_listCellRenderer != null) {
_listCellRenderer.setActualListRenderer(super.getCellRenderer());
return _listCellRenderer;
}
else {
return super.getCellRenderer();
}
}
public ListCellRenderer getActualCellRenderer() {
if (_listCellRenderer != null) {
return _listCellRenderer.getActualListRenderer();
}
else {
return super.getCellRenderer();
}
}
private void setSelectedObjects(Map<Object, String> selected) {
for (int i = 0; i < getModel().getSize(); i++) {
Object elementAt = getModel().getElementAt(i);
if (elementAt instanceof Selectable) {
Selectable selectable = (Selectable) elementAt;
if (selectable instanceof DefaultSelectable) {
elementAt = ((DefaultSelectable) selectable).getObject();
}
if (selected.get(elementAt) != null) {
selectable.setSelected(true);
fireItemStateChanged(new ItemEvent(this, ItemEvent.ITEM_STATE_CHANGED, selectable, ItemEvent.SELECTED));
selected.remove(elementAt);
if (selected.size() == 0) {
break;
}
}
else {
if (selectable.isSelected()) {
selectable.setSelected(false);
fireItemStateChanged(new ItemEvent(this, ItemEvent.ITEM_STATE_CHANGED, selectable, ItemEvent.DESELECTED));
}
}
}
}
repaint();
}
private static ListModel wrap(ListModel dataModel) {
for (int i = 0; i < dataModel.getSize(); i++) {
if (!(dataModel.getElementAt(i) instanceof Selectable)) {
throw new IllegalArgumentException("The ListModel contains an element which is not an instance of Selectable at index " + i + ".");
}
}
return dataModel;
}
private static Selectable[] wrap(Object[] objects) {
if (objects instanceof Selectable[]) {
return (Selectable[]) objects;
}
else {
Selectable[] elements = new Selectable[objects.length];
for (int i = 0; i < elements.length; i++) {
elements[i] = new DefaultSelectable(objects[i]);
}
return elements;
}
}
private static Vector<?> wrap(Vector<?> objects) {
Vector<Selectable> elements = new Vector<Selectable>();
for (Object o : objects) {
if (o instanceof Selectable) {
elements.add((Selectable) o);
}
else {
elements.add(new DefaultSelectable(o));
}
}
return elements;
}
/**
* Gets the value of property clickInCheckBoxOnly. If true, user can click on check boxes on each tree node to
* select and deselect. If false, user can't click but you as developer can programmatically call API to
* select/deselect it.
*
* @return the value of property clickInCheckBoxOnly.
*/
public boolean isClickInCheckBoxOnly() {
return _clickInCheckBoxOnly;
}
/**
* Sets the value of property clickInCheckBoxOnly.
*
* @param clickInCheckBoxOnly true to allow to check the check box. False to disable it which means user can see
* whether a row is checked or not but they cannot change it.
*/
public void setClickInCheckBoxOnly(boolean clickInCheckBoxOnly) {
if (clickInCheckBoxOnly != _clickInCheckBoxOnly) {
boolean old = _clickInCheckBoxOnly;
_clickInCheckBoxOnly = clickInCheckBoxOnly;
firePropertyChange(PROPERTY_CLICK_IN_CHECKBOX_ONLY, old, _clickInCheckBoxOnly);
}
}
protected static class Handler implements MouseListener, KeyListener {
protected CheckBoxListWithSelectable _list;
int _hotspot = new JCheckBox().getPreferredSize().width;
public Handler(CheckBoxListWithSelectable list) {
_list = list;
}
protected boolean clicksInCheckBox(MouseEvent e) {
int index = _list.locationToIndex(e.getPoint());
Rectangle bounds = _list.getCellBounds(index, index);
if (bounds != null) {
if (_list.getComponentOrientation().isLeftToRight()) {
return e.getX() < bounds.x + _hotspot;
}
else {
return e.getX() > bounds.x + bounds.width - _hotspot;
}
}
else {
return false;
}
}
public void mouseClicked(MouseEvent e) {
}
public void mousePressed(MouseEvent e) {
if (!_list.isCheckBoxEnabled() || !_list.isEnabled()) {
return;
}
if (!_list.isClickInCheckBoxOnly() || clicksInCheckBox(e)) {
int index = _list.locationToIndex(e.getPoint());
toggleSelection(index);
}
}
public void mouseReleased(MouseEvent e) {
}
public void mouseEntered(MouseEvent e) {
}
public void mouseExited(MouseEvent e) {
}
public void keyPressed(KeyEvent e) {
if (!_list.isCheckBoxEnabled() || !_list.isEnabled()) {
return;
}
if (e.getModifiers() == 0 && e.getKeyChar() == KeyEvent.VK_SPACE)
toggleSelections();
}
public void keyTyped(KeyEvent e) {
}
public void keyReleased(KeyEvent e) {
}
protected void toggleSelections() {
int[] indices = _list.getSelectedIndices();
ListModel model = _list.getModel();
for (int index : indices) {
Object element = model.getElementAt(index);
if (element instanceof Selectable && ((Selectable) element).isEnabled()) {
((Selectable) element).invertSelected();
boolean selected = ((Selectable) element).isSelected();
_list.fireItemStateChanged(new ItemEvent(_list, ItemEvent.ITEM_STATE_CHANGED, element, selected ? ItemEvent.SELECTED : ItemEvent.DESELECTED));
}
}
_list.repaint();
}
protected void toggleSelection(int index) {
ListModel model = _list.getModel();
if (index >= 0) {
Object element = model.getElementAt(index);
if (element instanceof Selectable && ((Selectable) element).isEnabled()) {
((Selectable) element).invertSelected();
boolean selected = ((Selectable) element).isSelected();
_list.fireItemStateChanged(new ItemEvent(_list, ItemEvent.ITEM_STATE_CHANGED, element, selected ? ItemEvent.SELECTED : ItemEvent.DESELECTED));
}
_list.repaint();
}
}
protected void toggleSelection() {
int index = _list.getSelectedIndex();
toggleSelection(index);
}
}
/**
* Adds a listener to the list that's notified each time a change to the item selection occurs. Listeners added
* directly to the <code>CheckBoxList</code> will have their <code>ItemEvent.getSource() == this
* CheckBoxList</code>.
*
* @param listener the <code>ItemListener</code> to add
*/
public void addItemListener(ItemListener listener) {
listenerList.add(ItemListener.class, listener);
}
/**
* Removes a listener from the list that's notified each time a change to the item selection occurs.
*
* @param listener the <code>ItemListener</code> to remove
*/
public void removeItemListener(ItemListener listener) {
listenerList.remove(ItemListener.class, listener);
}
/**
* Returns an array of all the <code>ItemListener</code>s added to this JList with addItemListener().
*
* @return all of the <code>ItemListener</code>s added or an empty array if no listeners have been added
*
* @see #addItemListener
*/
public ItemListener[] getItemListeners() {
return listenerList.getListeners(ItemListener.class);
}
/**
* Notifies all listeners that have registered interest for notification on this event type. The event instance is
* lazily created using the <code>event</code> parameter.
*
* @param event the <code>ItemEvent</code> object
* @see javax.swing.event.EventListenerList
*/
protected void fireItemStateChanged(ItemEvent event) {
// Guaranteed to return a non-null array
Object[] listeners = listenerList.getListenerList();
ItemEvent e = null;
// Process the listeners last to first, notifying
// those that are interested in this event
for (int i = listeners.length - 2; i >= 0; i -= 2) {
if (listeners[i] == ItemListener.class) {
// Lazily create the event:
if (e == null) {
e = new ItemEvent(CheckBoxListWithSelectable.this,
ItemEvent.ITEM_STATE_CHANGED,
event.getItem(),
event.getStateChange());
}
((ItemListener) listeners[i + 1]).itemStateChanged(e);
}
}
}
/**
* Gets the selected objects. This is different from {@link #getSelectedValues()} which is a JList's feature. The
* List returned from this method contains the objects that is checked in the CheckBoxList.
*
* @return the selected objects.
*/
public Object[] getSelectedObjects() {
Vector<Object> elements = new Vector<Object>();
for (int i = 0; i < getModel().getSize(); i++) {
Object elementAt = getModel().getElementAt(i);
if (elementAt instanceof Selectable) {
Selectable selectable = (Selectable) elementAt;
if (selectable.isSelected()) {
if (selectable instanceof DefaultSelectable) {
elements.add(((DefaultSelectable) selectable).getObject());
}
else {
elements.add(selectable);
}
}
}
}
return elements.toArray();
}
/**
* Selects all objects in this list except those are disabled.
*/
public void selectAll() {
for (int i = 0; i < getModel().getSize(); i++) {
Object elementAt = getModel().getElementAt(i);
if (elementAt instanceof Selectable) {
Selectable selectable = (Selectable) elementAt;
if (selectable.isEnabled() && !selectable.isSelected()) {
selectable.setSelected(true);
fireItemStateChanged(new ItemEvent(this, ItemEvent.ITEM_STATE_CHANGED, selectable, ItemEvent.SELECTED));
}
}
}
repaint();
}
/**
* Deselects all objects in this list except those are disabled.
*/
public void selectNone() {
for (int i = 0; i < getModel().getSize(); i++) {
Object elementAt = getModel().getElementAt(i);
if (elementAt instanceof Selectable) {
Selectable selectable = (Selectable) elementAt;
if (selectable.isEnabled() && selectable.isSelected()) {
selectable.setSelected(false);
fireItemStateChanged(new ItemEvent(this, ItemEvent.ITEM_STATE_CHANGED, selectable, ItemEvent.DESELECTED));
}
}
}
repaint();
}
@Override
public void setListData(Vector listData) {
super.setListData(wrap(listData));
}
@Override
public void setListData(Object[] listData) {
super.setListData(wrap(listData));
}
@Override
public int getNextMatch(String prefix, int startIndex, Position.Bias bias) {
return -1;
}
/**
* Gets the value of property checkBoxEnabled. If true, user can click on check boxes on each tree node to select
* and deselect. If false, user can't click but you as developer can programmatically call API to select/deselect
* it.
*
* @return the value of property checkBoxEnabled.
*/
public boolean isCheckBoxEnabled() {
return _checkBoxEnabled;
}
/**
* Checks if check box is visible. There is no setter for it. The only way is to override this method to return true
* or false.
*
* @param index the row index.
* @return true or false. If false, there is not check box on the particular row index.
*/
@SuppressWarnings({"UnusedDeclaration"})
public boolean isCheckBoxVisible(int index) {
return true;
}
/**
* Sets the value of property checkBoxEnabled.
*
* @param checkBoxEnabled true to enable all the check boxes. False to disable all of them.
*/
public void setCheckBoxEnabled(boolean checkBoxEnabled) {
if (checkBoxEnabled != _checkBoxEnabled) {
Boolean oldValue = _checkBoxEnabled ? Boolean.TRUE : Boolean.FALSE;
Boolean newValue = checkBoxEnabled ? Boolean.TRUE : Boolean.FALSE;
_checkBoxEnabled = checkBoxEnabled;
firePropertyChange(PROPERTY_CHECKBOX_ENABLED, oldValue, newValue);
repaint();
}
}
}