/**
* Copyright (C) 2015 Valkyrie RCP
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.valkyriercp.component;
import javax.swing.*;
import javax.swing.event.ListSelectionListener;
import java.awt.*;
import java.awt.event.ActionEvent;
import java.awt.event.ActionListener;
import java.awt.event.KeyAdapter;
import java.awt.event.KeyEvent;
import java.util.ArrayList;
import java.util.Collections;
import java.util.Comparator;
/**
* Custom panel that presents a "shuttle" list pair. One list is the "source"
* and the second list holds the "chosen" values from the source list. Buttons
* between the lists are used to move entries back and forth. By default, only
* the chosen list is displayed along with an Edit button. Pressing the edit
* button exposes the source list and the movement buttons.
* <p>
* This component essentially provides an alternate UI for a JList. It uses the
* same type of model and selection list. The selection is rendered as two lists
* instead of one list with highlighted entries. Those elements in the model
* that are not selected are shown in the source list and those that are
* selected are shown in the chosen list.
* <p>
* Normal selection model listeners are used to report changes to interested
* objects.
*
* @author lstreepy
* @author Benoit Xhenseval (Small modifications for text + icons config)
* @author Geoffrey De Smet
*/
public class ShuttleList extends JPanel {
private boolean showEditButton = false;
private JList helperList = new JList();
private JList sourceList = new JList();
private JLabel sourceLabel = new JLabel();
private JPanel sourcePanel = new JPanel(new BorderLayout());
private JPanel chosenPanel = new JPanel(new BorderLayout());
private JList chosenList = new JList();
private JLabel chosenLabel = new JLabel();
private JScrollPane helperScroller = new JScrollPane(helperList);
private JPanel buttonPanel;
private JButton editButton;
private ListModel dataModel;
private Comparator comparator;
private boolean panelsShowing;
private static final long serialVersionUID = -6038138479095186130L;
private JButton leftToRight;
private JButton allLeftToRight;
private JButton rightToLeft;
private JButton allRightToLeft;
/**
* Simple constructor.
*/
public ShuttleList() {
// The binder actually determines the default
this(true);
}
public ShuttleList(boolean showEditButton) {
this.showEditButton = showEditButton;
this.panelsShowing = !showEditButton;
buildComponent();
}
/**
* Returns the object that renders the list items.
*
* @return the <code>ListCellRenderer</code>
* @see #setCellRenderer
*/
public ListCellRenderer getCellRenderer() {
return sourceList.getCellRenderer();
}
/**
* Sets the delegate that's used to paint each cell in the list.
* <p>
* The default value of this property is provided by the ListUI delegate,
* i.e. by the look and feel implementation.
* <p>
*
* @param cellRenderer the <code>ListCellRenderer</code> that paints list
* cells
* @see #getCellRenderer
*/
public void setCellRenderer(ListCellRenderer cellRenderer) {
// Apply this to both lists
sourceList.setCellRenderer(cellRenderer);
chosenList.setCellRenderer(cellRenderer);
helperList.setCellRenderer(cellRenderer);
}
/**
* Returns the data model.
*
* @return the <code>ListModel</code> that provides the displayed list of
* items
*/
public ListModel getModel() {
return dataModel;
}
/**
* Sets the model that represents the contents or "value" of the list and
* clears the list selection.
*
* @param model the <code>ListModel</code> that provides the list of items
* for display
* @exception IllegalArgumentException if <code>model</code> is
* <code>null</code>
*/
public void setModel(ListModel model) {
helperList.setModel(model);
dataModel = model;
clearSelection();
// Once we have a model, we can properly size the two display lists
// They should be wide enough to hold the widest string in the model.
// So take the width of the source list since it currently has all the
// data.
Dimension d = helperScroller.getPreferredSize();
chosenPanel.setPreferredSize(d);
sourcePanel.setPreferredSize(d);
}
/**
* Sets the preferred number of rows in the list that can be displayed
* without a scrollbar.
*
* @param visibleRowCount an integer specifying the preferred number of
* visible rows
*/
public void setVisibleRowCount(int visibleRowCount) {
sourceList.setVisibleRowCount(visibleRowCount);
chosenList.setVisibleRowCount(visibleRowCount);
helperList.setVisibleRowCount(visibleRowCount);
// Ok, since we've haven't set a preferred size on the helper scroller,
// we can use it's current preferred size for our two control lists.
Dimension d = helperScroller.getPreferredSize();
chosenPanel.setPreferredSize(d);
sourcePanel.setPreferredSize(d);
}
/**
* Set the comparator to use for comparing list elements.
*
* @param comparator to use
*/
public void setComparator(Comparator comparator) {
this.comparator = comparator;
}
/**
* Set the icon to use on the edit button. If no icon is specified, then
* just the label will be used otherwise the text will be a tooltip.
*
* @param editIcon Icon to use on edit button
*/
public void setEditIcon(Icon editIcon, String text) {
if (editIcon != null) {
editButton.setIcon(editIcon);
if (text != null) {
editButton.setToolTipText(text);
}
editButton.setText("");
} else {
editButton.setIcon(null);
if (text != null) {
editButton.setText(text);
}
}
}
/**
* Add labels on top of the 2 lists. If not present, do not show the labels.
*
* @param chosenLabel
* @param sourceLabel
*/
public void setListLabels(String chosenLabel, String sourceLabel) {
if (chosenLabel != null) {
this.chosenLabel.setText(chosenLabel);
this.chosenLabel.setVisible(true);
} else {
this.chosenLabel.setVisible(false);
}
if (sourceLabel != null) {
this.sourceLabel.setText(sourceLabel);
this.sourceLabel.setVisible(true);
} else {
this.sourceLabel.setVisible(false);
}
Dimension d = chosenList.getPreferredSize();
Dimension d1 = this.chosenLabel.getPreferredSize();
Dimension dChosenPanel = chosenPanel.getPreferredSize();
dChosenPanel.width = Math.max(d.width, Math.max(dChosenPanel.width, d1.width));
chosenPanel.setPreferredSize(dChosenPanel);
Dimension dSourceList = sourceList.getPreferredSize();
Dimension dSource = this.sourceLabel.getPreferredSize();
Dimension dSourcePanel = sourcePanel.getPreferredSize();
dSourcePanel.width = Math.max(dSource.width, Math.max(dSourceList.width, dSourcePanel.width));
sourcePanel.setPreferredSize(dSourcePanel);
Dimension fullPanelSize = getPreferredSize();
fullPanelSize.width =
dSourcePanel.width + dChosenPanel.width + (editButton != null ? editButton.getPreferredSize().width : 0)
+ (buttonPanel != null ? buttonPanel.getPreferredSize().width : 0) + 20;
setPreferredSize(fullPanelSize);
}
/**
* Build our component panel.
*
* @return component
*/
protected JComponent buildComponent() {
helperList.setSelectionMode(ListSelectionModel.MULTIPLE_INTERVAL_SELECTION);
GridBagLayout gbl = new GridBagLayout();
GridBagConstraints gbc = new GridBagConstraints();
setLayout(gbl);
editButton = new JButton("Edit...");
editButton.setIconTextGap(0);
editButton.setMargin(new Insets(2, 4, 2, 4));
editButton.addActionListener(new ActionListener() {
public void actionPerformed(ActionEvent event) {
togglePanels();
}
});
gbc.fill = GridBagConstraints.NONE;
gbc.weightx = 0.0;
gbc.weighty = 0.0;
gbc.insets = new Insets(0, 0, 0, 3);
gbc.anchor = GridBagConstraints.NORTHWEST;
gbl.setConstraints(editButton, gbc);
add(editButton);
sourceList.setSelectionMode(ListSelectionModel.MULTIPLE_INTERVAL_SELECTION);
sourceList.addKeyListener(new KeyAdapter() {
public void keyPressed(final KeyEvent e) {
if (e.getKeyCode() == KeyEvent.VK_ENTER || e.getKeyCode() == KeyEvent.VK_RIGHT) {
moveLeftToRight();
}
}
});
gbc.fill = GridBagConstraints.BOTH;
gbc.weightx = 1.0;
gbc.weighty = 1.0;
sourcePanel.add(BorderLayout.NORTH, sourceLabel);
JScrollPane sourceScroller = new JScrollPane(sourceList);
sourcePanel.add(BorderLayout.CENTER, sourceScroller);
gbl.setConstraints(sourcePanel, gbc);
add(sourcePanel);
// JPanel buttonPanel = new ControlButtonPanel();
JPanel buttonPanel = buildButtonPanel();
gbc.fill = GridBagConstraints.VERTICAL;
gbc.weightx = 0;
gbc.weighty = 1.0;
gbl.setConstraints(buttonPanel, gbc);
add(buttonPanel);
chosenList.setSelectionMode(ListSelectionModel.MULTIPLE_INTERVAL_SELECTION);
chosenList.addKeyListener(new KeyAdapter() {
public void keyPressed(final KeyEvent e) {
if (e.getKeyCode() == KeyEvent.VK_ENTER || e.getKeyCode() == KeyEvent.VK_LEFT) {
moveRightToLeft();
}
}
});
gbc.fill = GridBagConstraints.BOTH;
gbc.weightx = 1.0;
gbc.weighty = 1.0;
chosenPanel.add(BorderLayout.NORTH, chosenLabel);
JScrollPane chosenScroller = new JScrollPane(chosenList);
chosenPanel.add(BorderLayout.CENTER, chosenScroller);
gbl.setConstraints(chosenPanel, gbc);
add(chosenPanel);
editButton.setVisible(showEditButton);
this.buttonPanel.setVisible(panelsShowing);
sourcePanel.setVisible(panelsShowing);
return this;
}
/**
* Construct the control button panel.
*
* @return JPanel
*
*/
protected JPanel buildButtonPanel() {
buttonPanel = new JPanel();
leftToRight = new JButton(">");
allLeftToRight = new JButton(">>");
rightToLeft = new JButton("<");
allRightToLeft = new JButton("<<");
Font smallerFont = leftToRight.getFont().deriveFont(9.0F);
leftToRight.setFont(smallerFont);
allLeftToRight.setFont(smallerFont);
rightToLeft.setFont(smallerFont);
allRightToLeft.setFont(smallerFont);
Insets margin = new Insets(2, 4, 2, 4);
leftToRight.setMargin(margin);
allLeftToRight.setMargin(margin);
rightToLeft.setMargin(margin);
allRightToLeft.setMargin(margin);
GridBagLayout gbl = new GridBagLayout();
GridBagConstraints gbc = new GridBagConstraints();
buttonPanel.setLayout(gbl);
gbc.gridwidth = GridBagConstraints.REMAINDER;
gbc.fill = GridBagConstraints.HORIZONTAL;
gbl.setConstraints(leftToRight, gbc);
gbl.setConstraints(allLeftToRight, gbc);
gbl.setConstraints(rightToLeft, gbc);
gbl.setConstraints(allRightToLeft, gbc);
buttonPanel.add(leftToRight);
buttonPanel.add(allLeftToRight);
buttonPanel.add(rightToLeft);
buttonPanel.add(allRightToLeft);
leftToRight.addActionListener(new ActionListener() {
public void actionPerformed(ActionEvent event) {
moveLeftToRight();
}
});
allLeftToRight.addActionListener(new ActionListener() {
public void actionPerformed(ActionEvent event) {
moveAllLeftToRight();
}
});
rightToLeft.addActionListener(new ActionListener() {
public void actionPerformed(ActionEvent event) {
moveRightToLeft();
}
});
allRightToLeft.addActionListener(new ActionListener() {
public void actionPerformed(ActionEvent event) {
moveAllRightToLeft();
}
});
buttonPanel.setBorder(BorderFactory.createEmptyBorder(2, 2, 2, 2));
// buttonPanel.setBackground( Color.lightGray );
return buttonPanel;
}
/**
* Toggle the panel visibility. This will hide/show the source list and
* movement buttons.
*/
public void togglePanels() {
panelsShowing = !panelsShowing;
sourcePanel.setVisible(panelsShowing);
buttonPanel.setVisible(panelsShowing);
}
/**
* Move the selected items in the source list to the chosen list. I.e., add
* the items to our selection model.
*/
protected void moveLeftToRight() {
// Loop over the selected items and locate them in the data model, Add
// these to the selection.
Object[] sourceSelected = sourceList.getSelectedValues();
int nSourceSelected = sourceSelected.length;
int[] currentSelection = helperList.getSelectedIndices();
int[] newSelection = new int[currentSelection.length + nSourceSelected];
System.arraycopy(currentSelection, 0, newSelection, 0, currentSelection.length);
int destPos = currentSelection.length;
for (int i = 0; i < sourceSelected.length; i++) {
newSelection[destPos++] = indexOf(sourceSelected[i]);
}
helperList.setSelectedIndices(newSelection);
update();
}
/**
* Move all the source items to the chosen side. I.e., select all the items.
*/
protected void moveAllLeftToRight() {
int sz = dataModel.getSize();
int[] selected = new int[sz];
for (int i = 0; i < sz; i++) {
selected[i] = i;
}
helperList.setSelectedIndices(selected);
update();
}
/**
* Move the selected items in the chosen list to the source list. I.e.,
* remove them from our selection model.
*/
protected void moveRightToLeft() {
Object[] chosenSelectedValues = chosenList.getSelectedValues();
int nChosenSelected = chosenSelectedValues.length;
int[] chosenSelected = new int[nChosenSelected];
if (nChosenSelected == 0) {
return; // Nothing to move
}
// Get our current selection
int[] currentSelected = helperList.getSelectedIndices();
int nCurrentSelected = currentSelected.length;
// Fill the chosenSelected array with the indices of the selected chosen
// items
for (int i = 0; i < nChosenSelected; i++) {
chosenSelected[i] = indexOf(chosenSelectedValues[i]);
}
// Construct the new selected indices. Loop through the current list
// and compare to the head of the chosen list. If not equal, then add
// to the new list. If equal, skip it and bump the head pointer on the
// chosen list.
int newSelection[] = new int[nCurrentSelected - nChosenSelected];
int newSelPos = 0;
int chosenPos = 0;
for (int i = 0; i < nCurrentSelected; i++) {
int currentIdx = currentSelected[i];
if (chosenPos < nChosenSelected && currentIdx == chosenSelected[chosenPos]) {
chosenPos += 1;
} else {
newSelection[newSelPos++] = currentIdx;
}
}
// Install the new selection
helperList.setSelectedIndices(newSelection);
update();
}
/**
* Move all the chosen items back to the source side. This simply sets our
* selection back to empty.
*/
protected void moveAllRightToLeft() {
clearSelection();
}
/**
* Get the index of a given object in the underlying data model.
*
* @param o Object to locate
* @return index of object in model, -1 if not found
*/
protected int indexOf(final Object o) {
final int size = dataModel.getSize();
for (int i = 0; i < size; i++) {
if (comparator == null) {
if (o.equals(dataModel.getElementAt(i))) {
return i;
}
} else if (comparator.compare(o, dataModel.getElementAt(i)) == 0) {
return i;
}
}
return -1;
}
/**
* Update the two lists based on the current selection indices.
*/
protected void update() {
int sz = dataModel.getSize();
int[] selected = helperList.getSelectedIndices();
ArrayList sourceItems = new ArrayList(sz);
ArrayList chosenItems = new ArrayList(selected.length);
// Start with the source items filled from our data model
for (int i = 0; i < sz; i++) {
sourceItems.add(dataModel.getElementAt(i));
}
// Now move the selected items to the chosen list
for (int i = selected.length - 1; i >= 0; i--) {
chosenItems.add(sourceItems.remove(selected[i]));
}
Collections.reverse(chosenItems); // We built it backwards
// Now install the two new lists
sourceList.setListData(sourceItems.toArray());
chosenList.setListData(chosenItems.toArray());
}
// ========================
// List Selection handling
// ========================
/**
* Returns the value of the current selection model.
*
* @return the <code>ListSelectionModel</code> that implements list
* selections
*/
public ListSelectionModel getSelectionModel() {
return helperList.getSelectionModel();
}
/**
* Adds a listener to the list that's notified each time a change to the
* selection occurs.
*
* @param listener the <code>ListSelectionListener</code> to add
*/
public void addListSelectionListener(ListSelectionListener listener) {
helperList.addListSelectionListener(listener);
}
/**
* Removes a listener from the list that's notified each time a change to
* the selection occurs.
*
* @param listener the <code>ListSelectionListener</code> to remove
*/
public void removeListSelectionListener(ListSelectionListener listener) {
helperList.removeListSelectionListener(listener);
}
/**
* Clear the selection. This will populate the source list with all the
* items from the model and empty the chosen list.
*/
public void clearSelection() {
helperList.clearSelection();
update();
}
/**
* Selects a set of cells.
*
* @param indices an array of the indices of the cells to select
*/
public void setSelectedIndices(int[] indices) {
helperList.setSelectedIndices(indices);
update();
}
/**
* Returns an array of the values for the selected cells. The returned
* values are sorted in increasing index order.
*
* @return the selected values or an empty list if nothing is selected
*/
public Object[] getSelectedValues() {
return helperList.getSelectedValues();
}
public void setEnabled(boolean enabled) {
super.setEnabled(enabled);
helperList.setEnabled(enabled);
sourceList.setEnabled(enabled);
chosenList.setEnabled(enabled);
buttonPanel.setEnabled(enabled);
leftToRight.setEnabled(enabled);
allLeftToRight.setEnabled(enabled);
rightToLeft.setEnabled(enabled);
allRightToLeft.setEnabled(enabled);
}
}