001 /**
002 *
003 */
004 package org.springframework.richclient.components;
005
006 import java.awt.BorderLayout;
007 import java.awt.Dimension;
008 import java.awt.Font;
009 import java.awt.GridBagConstraints;
010 import java.awt.GridBagLayout;
011 import java.awt.Insets;
012 import java.awt.event.ActionEvent;
013 import java.awt.event.ActionListener;
014 import java.awt.event.KeyAdapter;
015 import java.awt.event.KeyEvent;
016 import java.util.ArrayList;
017 import java.util.Collections;
018 import java.util.Comparator;
019
020 import javax.swing.BorderFactory;
021 import javax.swing.Icon;
022 import javax.swing.JButton;
023 import javax.swing.JComponent;
024 import javax.swing.JLabel;
025 import javax.swing.JList;
026 import javax.swing.JPanel;
027 import javax.swing.JScrollPane;
028 import javax.swing.ListCellRenderer;
029 import javax.swing.ListModel;
030 import javax.swing.ListSelectionModel;
031 import javax.swing.event.ListSelectionListener;
032
033 /**
034 * Custom panel that presents a "shuttle" list pair. One list is the "source"
035 * and the second list holds the "chosen" values from the source list. Buttons
036 * between the lists are used to move entries back and forth. By default, only
037 * the chosen list is displayed along with an Edit button. Pressing the edit
038 * button exposes the source list and the movement buttons.
039 * <p>
040 * This component essentially provides an alternate UI for a JList. It uses the
041 * same type of model and selection list. The selection is rendered as two lists
042 * instead of one list with highlighted entries. Those elements in the model
043 * that are not selected are shown in the source list and those that are
044 * selected are shown in the chosen list.
045 * <p>
046 * Normal selection model listeners are used to report changes to interested
047 * objects.
048 *
049 * @author lstreepy
050 * @author Benoit Xhenseval (Small modifications for text + icons config)
051 * @author Geoffrey De Smet
052 */
053 public class ShuttleList extends JPanel {
054
055 private boolean showEditButton = false;
056
057 private JList helperList = new JList();
058
059 private JList sourceList = new JList();
060
061 private JLabel sourceLabel = new JLabel();
062
063 private JPanel sourcePanel = new JPanel(new BorderLayout());
064
065 private JPanel chosenPanel = new JPanel(new BorderLayout());
066
067 private JList chosenList = new JList();
068
069 private JLabel chosenLabel = new JLabel();
070
071 private JScrollPane helperScroller = new JScrollPane(helperList);
072
073 private JPanel buttonPanel;
074
075 private JButton editButton;
076
077 private ListModel dataModel;
078
079 private Comparator comparator;
080
081 private boolean panelsShowing;
082
083 private static final long serialVersionUID = -6038138479095186130L;
084
085 private JButton leftToRight;
086
087 private JButton allLeftToRight;
088
089 private JButton rightToLeft;
090
091 private JButton allRightToLeft;
092
093 /**
094 * Simple constructor.
095 */
096 public ShuttleList() {
097 // The binder actually determines the default
098 this(true);
099 }
100
101 public ShuttleList(boolean showEditButton) {
102 this.showEditButton = showEditButton;
103 this.panelsShowing = !showEditButton;
104 buildComponent();
105 }
106
107 /**
108 * Returns the object that renders the list items.
109 *
110 * @return the <code>ListCellRenderer</code>
111 * @see #setCellRenderer
112 */
113 public ListCellRenderer getCellRenderer() {
114 return sourceList.getCellRenderer();
115 }
116
117 /**
118 * Sets the delegate that's used to paint each cell in the list.
119 * <p>
120 * The default value of this property is provided by the ListUI delegate,
121 * i.e. by the look and feel implementation.
122 * <p>
123 *
124 * @param cellRenderer the <code>ListCellRenderer</code> that paints list
125 * cells
126 * @see #getCellRenderer
127 */
128 public void setCellRenderer(ListCellRenderer cellRenderer) {
129 // Apply this to both lists
130 sourceList.setCellRenderer(cellRenderer);
131 chosenList.setCellRenderer(cellRenderer);
132 helperList.setCellRenderer(cellRenderer);
133 }
134
135 /**
136 * Returns the data model.
137 *
138 * @return the <code>ListModel</code> that provides the displayed list of
139 * items
140 */
141 public ListModel getModel() {
142 return dataModel;
143 }
144
145 /**
146 * Sets the model that represents the contents or "value" of the list and
147 * clears the list selection.
148 *
149 * @param model the <code>ListModel</code> that provides the list of items
150 * for display
151 * @exception IllegalArgumentException if <code>model</code> is
152 * <code>null</code>
153 */
154 public void setModel(ListModel model) {
155 helperList.setModel(model);
156
157 dataModel = model;
158 clearSelection();
159
160 // Once we have a model, we can properly size the two display lists
161 // They should be wide enough to hold the widest string in the model.
162 // So take the width of the source list since it currently has all the
163 // data.
164 Dimension d = helperScroller.getPreferredSize();
165 chosenPanel.setPreferredSize(d);
166 sourcePanel.setPreferredSize(d);
167 }
168
169 /**
170 * Sets the preferred number of rows in the list that can be displayed
171 * without a scrollbar.
172 *
173 * @param visibleRowCount an integer specifying the preferred number of
174 * visible rows
175 */
176 public void setVisibleRowCount(int visibleRowCount) {
177 sourceList.setVisibleRowCount(visibleRowCount);
178 chosenList.setVisibleRowCount(visibleRowCount);
179 helperList.setVisibleRowCount(visibleRowCount);
180
181 // Ok, since we've haven't set a preferred size on the helper scroller,
182 // we can use it's current preferred size for our two control lists.
183 Dimension d = helperScroller.getPreferredSize();
184 chosenPanel.setPreferredSize(d);
185 sourcePanel.setPreferredSize(d);
186
187 }
188
189 /**
190 * Set the comparator to use for comparing list elements.
191 *
192 * @param comparator to use
193 */
194 public void setComparator(Comparator comparator) {
195 this.comparator = comparator;
196 }
197
198 /**
199 * Set the icon to use on the edit button. If no icon is specified, then
200 * just the label will be used otherwise the text will be a tooltip.
201 *
202 * @param editIcon Icon to use on edit button
203 */
204 public void setEditIcon(Icon editIcon, String text) {
205 if (editIcon != null) {
206 editButton.setIcon(editIcon);
207 if (text != null) {
208 editButton.setToolTipText(text);
209 }
210 editButton.setText("");
211 } else {
212 editButton.setIcon(null);
213 if (text != null) {
214 editButton.setText(text);
215 }
216 }
217 }
218
219 /**
220 * Add labels on top of the 2 lists. If not present, do not show the labels.
221 *
222 * @param chosenLabel
223 * @param sourceLabel
224 */
225 public void setListLabels(String chosenLabel, String sourceLabel) {
226 if (chosenLabel != null) {
227 this.chosenLabel.setText(chosenLabel);
228 this.chosenLabel.setVisible(true);
229 } else {
230 this.chosenLabel.setVisible(false);
231 }
232
233 if (sourceLabel != null) {
234 this.sourceLabel.setText(sourceLabel);
235 this.sourceLabel.setVisible(true);
236 } else {
237 this.sourceLabel.setVisible(false);
238 }
239
240 Dimension d = chosenList.getPreferredSize();
241 Dimension d1 = this.chosenLabel.getPreferredSize();
242 Dimension dChosenPanel = chosenPanel.getPreferredSize();
243
244 dChosenPanel.width = Math.max(d.width, Math.max(dChosenPanel.width, d1.width));
245 chosenPanel.setPreferredSize(dChosenPanel);
246
247 Dimension dSourceList = sourceList.getPreferredSize();
248 Dimension dSource = this.sourceLabel.getPreferredSize();
249 Dimension dSourcePanel = sourcePanel.getPreferredSize();
250 dSourcePanel.width = Math.max(dSource.width, Math.max(dSourceList.width, dSourcePanel.width));
251 sourcePanel.setPreferredSize(dSourcePanel);
252
253 Dimension fullPanelSize = getPreferredSize();
254 fullPanelSize.width =
255 dSourcePanel.width + dChosenPanel.width + (editButton != null ? editButton.getPreferredSize().width : 0)
256 + (buttonPanel != null ? buttonPanel.getPreferredSize().width : 0) + 20;
257 setPreferredSize(fullPanelSize);
258 }
259
260 /**
261 * Build our component panel.
262 *
263 * @return component
264 */
265 protected JComponent buildComponent() {
266 helperList.setSelectionMode(ListSelectionModel.MULTIPLE_INTERVAL_SELECTION);
267
268 GridBagLayout gbl = new GridBagLayout();
269 GridBagConstraints gbc = new GridBagConstraints();
270
271 setLayout(gbl);
272
273 editButton = new JButton("Edit...");
274 editButton.setIconTextGap(0);
275 editButton.setMargin(new Insets(2, 4, 2, 4));
276
277 editButton.addActionListener(new ActionListener() {
278 public void actionPerformed(ActionEvent event) {
279 togglePanels();
280 }
281 });
282
283 gbc.fill = GridBagConstraints.NONE;
284 gbc.weightx = 0.0;
285 gbc.weighty = 0.0;
286 gbc.insets = new Insets(0, 0, 0, 3);
287 gbc.anchor = GridBagConstraints.NORTHWEST;
288 gbl.setConstraints(editButton, gbc);
289 add(editButton);
290
291 sourceList.setSelectionMode(ListSelectionModel.MULTIPLE_INTERVAL_SELECTION);
292 sourceList.addKeyListener(new KeyAdapter() {
293 public void keyPressed(final KeyEvent e) {
294 if (e.getKeyCode() == KeyEvent.VK_ENTER || e.getKeyCode() == KeyEvent.VK_RIGHT) {
295 moveLeftToRight();
296 }
297 }
298 });
299
300 gbc.fill = GridBagConstraints.BOTH;
301 gbc.weightx = 1.0;
302 gbc.weighty = 1.0;
303 sourcePanel.add(BorderLayout.NORTH, sourceLabel);
304 JScrollPane sourceScroller = new JScrollPane(sourceList);
305 sourcePanel.add(BorderLayout.CENTER, sourceScroller);
306 gbl.setConstraints(sourcePanel, gbc);
307 add(sourcePanel);
308
309 // JPanel buttonPanel = new ControlButtonPanel();
310 JPanel buttonPanel = buildButtonPanel();
311 gbc.fill = GridBagConstraints.VERTICAL;
312 gbc.weightx = 0;
313 gbc.weighty = 1.0;
314 gbl.setConstraints(buttonPanel, gbc);
315 add(buttonPanel);
316
317 chosenList.setSelectionMode(ListSelectionModel.MULTIPLE_INTERVAL_SELECTION);
318 chosenList.addKeyListener(new KeyAdapter() {
319 public void keyPressed(final KeyEvent e) {
320 if (e.getKeyCode() == KeyEvent.VK_ENTER || e.getKeyCode() == KeyEvent.VK_LEFT) {
321 moveRightToLeft();
322 }
323 }
324 });
325 gbc.fill = GridBagConstraints.BOTH;
326 gbc.weightx = 1.0;
327 gbc.weighty = 1.0;
328 chosenPanel.add(BorderLayout.NORTH, chosenLabel);
329 JScrollPane chosenScroller = new JScrollPane(chosenList);
330 chosenPanel.add(BorderLayout.CENTER, chosenScroller);
331 gbl.setConstraints(chosenPanel, gbc);
332 add(chosenPanel);
333
334 editButton.setVisible(showEditButton);
335 this.buttonPanel.setVisible(panelsShowing);
336 sourcePanel.setVisible(panelsShowing);
337
338 return this;
339 }
340
341 /**
342 * Construct the control button panel.
343 *
344 * @return JPanel
345 *
346 */
347 protected JPanel buildButtonPanel() {
348 buttonPanel = new JPanel();
349
350 leftToRight = new JButton(">");
351 allLeftToRight = new JButton(">>");
352 rightToLeft = new JButton("<");
353 allRightToLeft = new JButton("<<");
354 Font smallerFont = leftToRight.getFont().deriveFont(9.0F);
355 leftToRight.setFont(smallerFont);
356 allLeftToRight.setFont(smallerFont);
357 rightToLeft.setFont(smallerFont);
358 allRightToLeft.setFont(smallerFont);
359
360 Insets margin = new Insets(2, 4, 2, 4);
361 leftToRight.setMargin(margin);
362 allLeftToRight.setMargin(margin);
363 rightToLeft.setMargin(margin);
364 allRightToLeft.setMargin(margin);
365
366 GridBagLayout gbl = new GridBagLayout();
367 GridBagConstraints gbc = new GridBagConstraints();
368
369 buttonPanel.setLayout(gbl);
370
371 gbc.gridwidth = GridBagConstraints.REMAINDER;
372 gbc.fill = GridBagConstraints.HORIZONTAL;
373 gbl.setConstraints(leftToRight, gbc);
374 gbl.setConstraints(allLeftToRight, gbc);
375 gbl.setConstraints(rightToLeft, gbc);
376 gbl.setConstraints(allRightToLeft, gbc);
377
378 buttonPanel.add(leftToRight);
379 buttonPanel.add(allLeftToRight);
380 buttonPanel.add(rightToLeft);
381 buttonPanel.add(allRightToLeft);
382
383 leftToRight.addActionListener(new ActionListener() {
384 public void actionPerformed(ActionEvent event) {
385 moveLeftToRight();
386 }
387 });
388 allLeftToRight.addActionListener(new ActionListener() {
389 public void actionPerformed(ActionEvent event) {
390 moveAllLeftToRight();
391 }
392 });
393 rightToLeft.addActionListener(new ActionListener() {
394 public void actionPerformed(ActionEvent event) {
395 moveRightToLeft();
396 }
397 });
398 allRightToLeft.addActionListener(new ActionListener() {
399 public void actionPerformed(ActionEvent event) {
400 moveAllRightToLeft();
401 }
402 });
403
404 buttonPanel.setBorder(BorderFactory.createEmptyBorder(2, 2, 2, 2));
405 // buttonPanel.setBackground( Color.lightGray );
406 return buttonPanel;
407 }
408
409 /**
410 * Toggle the panel visibility. This will hide/show the source list and
411 * movement buttons.
412 */
413 public void togglePanels() {
414 panelsShowing = !panelsShowing;
415 sourcePanel.setVisible(panelsShowing);
416 buttonPanel.setVisible(panelsShowing);
417 }
418
419 /**
420 * Move the selected items in the source list to the chosen list. I.e., add
421 * the items to our selection model.
422 */
423 protected void moveLeftToRight() {
424 // Loop over the selected items and locate them in the data model, Add
425 // these to the selection.
426 Object[] sourceSelected = sourceList.getSelectedValues();
427 int nSourceSelected = sourceSelected.length;
428 int[] currentSelection = helperList.getSelectedIndices();
429 int[] newSelection = new int[currentSelection.length + nSourceSelected];
430 System.arraycopy(currentSelection, 0, newSelection, 0, currentSelection.length);
431 int destPos = currentSelection.length;
432
433 for (int i = 0; i < sourceSelected.length; i++) {
434 newSelection[destPos++] = indexOf(sourceSelected[i]);
435 }
436
437 helperList.setSelectedIndices(newSelection);
438 update();
439 }
440
441 /**
442 * Move all the source items to the chosen side. I.e., select all the items.
443 */
444 protected void moveAllLeftToRight() {
445 int sz = dataModel.getSize();
446 int[] selected = new int[sz];
447 for (int i = 0; i < sz; i++) {
448 selected[i] = i;
449 }
450 helperList.setSelectedIndices(selected);
451 update();
452 }
453
454 /**
455 * Move the selected items in the chosen list to the source list. I.e.,
456 * remove them from our selection model.
457 */
458 protected void moveRightToLeft() {
459 Object[] chosenSelectedValues = chosenList.getSelectedValues();
460 int nChosenSelected = chosenSelectedValues.length;
461 int[] chosenSelected = new int[nChosenSelected];
462
463 if (nChosenSelected == 0) {
464 return; // Nothing to move
465 }
466
467 // Get our current selection
468 int[] currentSelected = helperList.getSelectedIndices();
469 int nCurrentSelected = currentSelected.length;
470
471 // Fill the chosenSelected array with the indices of the selected chosen
472 // items
473 for (int i = 0; i < nChosenSelected; i++) {
474 chosenSelected[i] = indexOf(chosenSelectedValues[i]);
475 }
476
477 // Construct the new selected indices. Loop through the current list
478 // and compare to the head of the chosen list. If not equal, then add
479 // to the new list. If equal, skip it and bump the head pointer on the
480 // chosen list.
481
482 int newSelection[] = new int[nCurrentSelected - nChosenSelected];
483 int newSelPos = 0;
484 int chosenPos = 0;
485
486 for (int i = 0; i < nCurrentSelected; i++) {
487 int currentIdx = currentSelected[i];
488 if (chosenPos < nChosenSelected && currentIdx == chosenSelected[chosenPos]) {
489 chosenPos += 1;
490 } else {
491 newSelection[newSelPos++] = currentIdx;
492 }
493 }
494
495 // Install the new selection
496 helperList.setSelectedIndices(newSelection);
497 update();
498 }
499
500 /**
501 * Move all the chosen items back to the source side. This simply sets our
502 * selection back to empty.
503 */
504 protected void moveAllRightToLeft() {
505 clearSelection();
506 }
507
508 /**
509 * Get the index of a given object in the underlying data model.
510 *
511 * @param o Object to locate
512 * @return index of object in model, -1 if not found
513 */
514 protected int indexOf(final Object o) {
515 final int size = dataModel.getSize();
516 for (int i = 0; i < size; i++) {
517 if (comparator == null) {
518 if (o.equals(dataModel.getElementAt(i))) {
519 return i;
520 }
521 } else if (comparator.compare(o, dataModel.getElementAt(i)) == 0) {
522 return i;
523 }
524 }
525
526 return -1;
527 }
528
529 /**
530 * Update the two lists based on the current selection indices.
531 */
532 protected void update() {
533 int sz = dataModel.getSize();
534 int[] selected = helperList.getSelectedIndices();
535 ArrayList sourceItems = new ArrayList(sz);
536 ArrayList chosenItems = new ArrayList(selected.length);
537
538 // Start with the source items filled from our data model
539 for (int i = 0; i < sz; i++) {
540 sourceItems.add(dataModel.getElementAt(i));
541 }
542
543 // Now move the selected items to the chosen list
544 for (int i = selected.length - 1; i >= 0; i--) {
545 chosenItems.add(sourceItems.remove(selected[i]));
546 }
547
548 Collections.reverse(chosenItems); // We built it backwards
549
550 // Now install the two new lists
551 sourceList.setListData(sourceItems.toArray());
552 chosenList.setListData(chosenItems.toArray());
553 }
554
555 // ========================
556 // List Selection handling
557 // ========================
558
559 /**
560 * Returns the value of the current selection model.
561 *
562 * @return the <code>ListSelectionModel</code> that implements list
563 * selections
564 */
565 public ListSelectionModel getSelectionModel() {
566 return helperList.getSelectionModel();
567 }
568
569 /**
570 * Adds a listener to the list that's notified each time a change to the
571 * selection occurs.
572 *
573 * @param listener the <code>ListSelectionListener</code> to add
574 */
575 public void addListSelectionListener(ListSelectionListener listener) {
576 helperList.addListSelectionListener(listener);
577 }
578
579 /**
580 * Removes a listener from the list that's notified each time a change to
581 * the selection occurs.
582 *
583 * @param listener the <code>ListSelectionListener</code> to remove
584 */
585 public void removeListSelectionListener(ListSelectionListener listener) {
586 helperList.removeListSelectionListener(listener);
587 }
588
589 /**
590 * Clear the selection. This will populate the source list with all the
591 * items from the model and empty the chosen list.
592 */
593 public void clearSelection() {
594 helperList.clearSelection();
595 update();
596 }
597
598 /**
599 * Selects a set of cells.
600 *
601 * @param indices an array of the indices of the cells to select
602 */
603 public void setSelectedIndices(int[] indices) {
604 helperList.setSelectedIndices(indices);
605 update();
606 }
607
608 /**
609 * Returns an array of the values for the selected cells. The returned
610 * values are sorted in increasing index order.
611 *
612 * @return the selected values or an empty list if nothing is selected
613 */
614 public Object[] getSelectedValues() {
615 return helperList.getSelectedValues();
616 }
617
618 public void setEnabled(boolean enabled) {
619 super.setEnabled(enabled);
620 helperList.setEnabled(enabled);
621 sourceList.setEnabled(enabled);
622 chosenList.setEnabled(enabled);
623 buttonPanel.setEnabled(enabled);
624 leftToRight.setEnabled(enabled);
625 allLeftToRight.setEnabled(enabled);
626 rightToLeft.setEnabled(enabled);
627 allRightToLeft.setEnabled(enabled);
628 }
629 }