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    }