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 }