001: /*
002: * GeoTools - OpenSource mapping toolkit
003: * http://geotools.org
004: * (C) 2003-2006, Geotools Project Managment Committee (PMC)
005: * (C) 2002, Institut de Recherche pour le Développement
006: *
007: * This library is free software; you can redistribute it and/or
008: * modify it under the terms of the GNU Lesser General Public
009: * License as published by the Free Software Foundation; either
010: * version 2.1 of the License, or (at your option) any later version.
011: *
012: * This library is distributed in the hope that it will be useful,
013: * but WITHOUT ANY WARRANTY; without even the implied warranty of
014: * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
015: * Lesser General Public License for more details.
016: */
017: package org.geotools.gui.swing;
018:
019: // Swing and AWT dependencies
020: import java.awt.Font;
021: import javax.swing.JList;
022: import javax.swing.JPanel;
023: import javax.swing.JButton;
024: import javax.swing.JScrollPane;
025: import javax.swing.AbstractListModel;
026: import java.awt.IllegalComponentStateException;
027:
028: // Events
029: import java.awt.event.ActionEvent;
030: import java.awt.event.ActionListener;
031:
032: // Layout
033: import java.awt.Dimension;
034: import java.awt.Component;
035: import java.awt.GridBagLayout;
036: import java.awt.GridBagConstraints;
037: import java.util.List;
038: import java.util.Iterator;
039: import java.util.ArrayList;
040: import java.util.Collection;
041: import java.util.Collections;
042: import java.util.Arrays;
043: import java.util.Locale;
044:
045: // Geotools dependencies
046: import org.geotools.resources.XArray;
047: import org.geotools.resources.Utilities;
048: import org.geotools.resources.SwingUtilities;
049:
050: /**
051: * A widget showing selected and unselected items in two disjoint list. The list on the left
052: * side shows items available for selection. The list on the right side shows items already
053: * selected. User can move items from one list to the other using buttons in the middle.
054: *
055: * @since 2.0
056: * @source $URL: http://svn.geotools.org/geotools/tags/2.4.1/modules/extension/widgets-swing/src/main/java/org/geotools/gui/swing/DisjointLists.java $
057: * @version $Id: DisjointLists.java 22112 2006-10-13 19:43:34Z desruisseaux $
058: * @author Martin Desruisseaux
059: */
060: public class DisjointLists extends JPanel {
061: /**
062: * The list model. Each {@link DisjointLists} object will use two instances
063: * of this class. Both instances share the same list of elements, but have
064: * their own list of index of visibles elements.
065: */
066: private static final class Model extends AbstractListModel {
067: /**
068: * The list of elements shared by both lists. Not all elements in this list will be
069: * displayed. The index of elements to shown are enumerated in the {@link #visibles}
070: * array.
071: * <p>
072: * Note: this list is read by {@link DisjointLists#selectElements}. The content
073: * of this list should never be modified from any method outside this class.
074: */
075: final List choices;
076:
077: /**
078: * The index of valids elements in the {@link #choice} list. This array will growth
079: * as needed. Elements in this array should always be in strictly increasing order.
080: */
081: private int[] visibles = new int[12];
082:
083: /**
084: * The number of valid elements in the {@link #visibles} array.
085: */
086: private int size;
087:
088: /**
089: * Constructs a model for the specified list of elements.
090: */
091: public Model(final List choices) {
092: this .choices = choices;
093: }
094:
095: /**
096: * Returns {@code true} if all elements in the {@link #visible} array
097: * are in strictly increasing order. This is used for assertions.
098: */
099: private boolean isSorted() {
100: for (int i = 1; i < size; i++) {
101: if (visibles[i] <= visibles[i - 1]) {
102: return false;
103: }
104: }
105: return true;
106: }
107:
108: /**
109: * Searchs the insertion point. We should use Arrays.binarySearch(...), but
110: * unfortunatly J2SE 1.4 do not provides an API for searching in a subarray.
111: */
112: private static int search(final int[] array, int lower,
113: final int upper, final int value) {
114: while (lower < upper && value > array[lower]) {
115: lower++;
116: }
117: return lower;
118: }
119:
120: /**
121: * Returns the number of valid elements.
122: */
123: public int getSize() {
124: assert size >= 0 && size <= choices.size() : size;
125: return size;
126: }
127:
128: /**
129: * Returns all elements in this list.
130: */
131: public Collection getElements() {
132: final Object[] list = new Object[getSize()];
133: for (int i = 0; i < list.length; i++) {
134: list[i] = ListElement.unwrap(getElementAt(i));
135: }
136: return Arrays.asList(list);
137: }
138:
139: /**
140: * Returns the element at the specified index.
141: */
142: public Object getElementAt(final int index) {
143: assert index >= 0 && index < size : index;
144: return choices.get(visibles[index]);
145: }
146:
147: /**
148: * Makes sure that the {@link #visibles} array has the specified capacity.
149: */
150: private void ensureCapacity(final int capacity) {
151: if (visibles.length < capacity) {
152: visibles = XArray.resize(visibles, Math.max(size * 2,
153: capacity));
154: }
155: }
156:
157: /**
158: * Removes a range of visible elements. The {@code lower} and {@code upper}
159: * indices are index (not values) in the {@link #visibles} array.
160: */
161: private void hide(final int lower, final int upper) {
162: if (lower != upper) {
163: System.arraycopy(visibles, upper, visibles, lower, size
164: - upper);
165: size -= (upper - lower);
166: fireIntervalRemoved(this , lower, upper - 1);
167: }
168: assert isSorted();
169: }
170:
171: /**
172: * Moves elements in the specified range from the specified model to this model.
173: *
174: * @param source The source model.
175: * @param lower Lower index (inclusive) in the source model.
176: * @param upper Upper index (exclusive) in the source model.
177: */
178: public void move(final Model source, final int lower,
179: final int upper) {
180: assert lower >= 0 && upper <= source.size;
181: ensureCapacity(size + (upper - lower));
182: int insertAt = 0;
183: int subUpper = lower;
184: while (subUpper < upper) {
185: final int subLower = subUpper;
186: assert isSorted();
187: insertAt = search(visibles, insertAt, size,
188: source.visibles[subLower]);
189: if (insertAt == size) {
190: subUpper = upper;
191: } else {
192: subUpper = search(source.visibles, subLower, upper,
193: visibles[insertAt]);
194: }
195: final int length = subUpper - subLower;
196: System.arraycopy(visibles, insertAt, visibles, insertAt
197: + length, size - insertAt);
198: System.arraycopy(source.visibles, subLower, visibles,
199: insertAt, length);
200: size += length;
201: assert isSorted();
202: fireIntervalAdded(this , insertAt, insertAt + length - 1);
203: }
204: source.hide(lower, upper);
205: }
206:
207: /**
208: * Moves elements at the specified indices from the specified model to this model.
209: * Note: the indice array will be overwritten.
210: *
211: * @param source The source model.
212: * @param indices Indices of elements in the source model to move.
213: */
214: public void move(final Model source, final int[] indices) {
215: Arrays.sort(indices);
216: for (int i = 0; i < indices.length;) {
217: int lower = indices[i];
218: int upper = lower + 1;
219: while (++i < indices.length && indices[i] == upper) {
220: // Collapses consecutive indices in a single move operation.
221: upper++;
222: }
223: move(source, lower, upper);
224: final int length = (upper - lower);
225: for (int j = i; j < indices.length; j++) {
226: // Adjusts the remaining indices. Since we just moved previous
227: // elements, the indices of remaining elements are shifted.
228: indices[j] -= length;
229: }
230: }
231: }
232:
233: /**
234: * Adds all elements from the specified collection.
235: */
236: public void addAll(final Collection items) {
237: if (!items.isEmpty()) {
238: choices.addAll(items);
239: final int length = items.size();
240: ensureCapacity(size + length);
241: final int max = choices.size();
242: for (int i = max - length; i < max; i++) {
243: visibles[size++] = i;
244: }
245: assert isSorted();
246: fireIntervalAdded(this , size - length, size - 1);
247: }
248: }
249:
250: /**
251: * Removes all elements from this model.
252: */
253: public void clear() {
254: choices.clear();
255: if (size != 0) {
256: final int oldSize = size;
257: size = 0;
258: fireIntervalRemoved(this , 0, oldSize - 1);
259: }
260: }
261: }
262:
263: /**
264: * Action invoked when the user pressed a button. This action
265: * invokes {@link Model#move} with selected indices.
266: */
267: private static final class Action implements ActionListener {
268: /**
269: * The source and target lists.
270: */
271: private final JList source, target;
272:
273: /**
274: * {@code true} if we should move all items on action.
275: */
276: private final boolean all;
277:
278: /**
279: * Constructs a new "move" action.
280: */
281: public Action(final JList source, final JList target,
282: final boolean all) {
283: this .source = source;
284: this .target = target;
285: this .all = all;
286: }
287:
288: /**
289: * Invoked when the user pressed a "move" button.
290: */
291: public void actionPerformed(final ActionEvent event) {
292: final Model source = (Model) this .source.getModel();
293: final Model target = (Model) this .target.getModel();
294: if (all) {
295: target.move(source, 0, source.getSize());
296: return;
297: }
298: final int[] indices = this .source.getSelectedIndices();
299: target.move(source, indices);
300: }
301: }
302:
303: /**
304: * The list on the left side. This is the list that contains
305: * the element selectable by the user.
306: */
307: private final JList left;
308:
309: /**
310: * The list on the right side. This list is initially empty.
311: */
312: private final JList right;
313:
314: /**
315: * {@code true} if elements should be automatically sorted.
316: */
317: private boolean autoSort = true;
318:
319: /**
320: * Construct a new list.
321: */
322: public DisjointLists() {
323: super (new GridBagLayout());
324: /*
325: * Setup lists
326: */
327: final List choices = new ArrayList();
328: left = new JList(new Model(choices));
329: right = new JList(new Model(choices));
330: final JScrollPane leftPane = new JScrollPane(left);
331: final JScrollPane rightPane = new JScrollPane(right);
332: final Dimension size = new Dimension(160, 200);
333: leftPane.setPreferredSize(size);
334: rightPane.setPreferredSize(size);
335: /*
336: * Setup buttons
337: */
338: final JButton add = getButton("StepForward", ">",
339: "Add selected elements");
340: final JButton remove = getButton("StepBack", "<",
341: "Remove selected elements");
342: final JButton addAll = getButton("FastForward", ">>", "Add all");
343: final JButton removeAll = getButton("Rewind", "<<",
344: "Remove all");
345: add.addActionListener(new Action(left, right, false));
346: remove.addActionListener(new Action(right, left, false));
347: addAll.addActionListener(new Action(left, right, true));
348: removeAll.addActionListener(new Action(right, left, true));
349: /*
350: * Build UI
351: */
352: final GridBagConstraints c = new GridBagConstraints();
353: c.gridy = 0;
354: c.gridwidth = 1;
355: c.gridheight = 4;
356: c.weightx = c.weighty = 1;
357: c.fill = c.BOTH;
358: c.gridx = 0;
359: add(leftPane, c);
360: c.gridx = 2;
361: add(rightPane, c);
362:
363: c.insets.left = c.insets.right = 9;
364: c.gridx = 1;
365: c.gridheight = 1;
366: c.weightx = 0;
367: c.fill = c.HORIZONTAL;
368: c.gridy = 0;
369: c.anchor = c.SOUTH;
370: add(add, c);
371: c.gridy = 3;
372: c.anchor = c.NORTH;
373: add(removeAll, c);
374: c.gridy = 2;
375: c.weighty = 0;
376: add(addAll, c);
377: c.gridy = 1;
378: c.insets.bottom = 9;
379: add(remove, c);
380: }
381:
382: /**
383: * Returns a button.
384: *
385: * @param loader The class loader for loading the button's image.
386: * @param image The image name to load in the "media" category from the
387: * <A HREF="http://developer.java.sun.com/developer/techDocs/hi/repository/">Swing
388: * graphics repository</A>.
389: * @param fallback The fallback to use if the image is not found.
390: * @param description a brief description to use for tooltips.
391: * @return The button.
392: */
393: private static JButton getButton(String image,
394: final String fallback, final String description) {
395: image = "toolbarButtonGraphics/media/" + image + "16.gif";
396: return IconFactory.DEFAULT.getButton(image, description,
397: fallback);
398: }
399:
400: /**
401: * Returns {@code true} if elements are automatically sorted when added to this list.
402: * The default value is {@code true}.
403: *
404: * @since 2.2
405: */
406: public boolean isAutoSortEnabled() {
407: return autoSort;
408: }
409:
410: /**
411: * Sets to {@code true} if elements should be automatically sorted when added to this list.
412: *
413: * @since 2.2
414: */
415: public void setAutoSortEnabled(final boolean autoSort) {
416: if (autoSort != this .autoSort) {
417: this .autoSort = autoSort;
418: if (autoSort) {
419: final List elements = new ArrayList(((Model) left
420: .getModel()).choices);
421: clear();
422: addElements(elements);
423: }
424: firePropertyChange("autoSort", !autoSort, autoSort);
425: }
426: }
427:
428: /**
429: * Removes all elements from this list.
430: *
431: * @since 2.2
432: */
433: public void clear() {
434: ((Model) left.getModel()).clear();
435: ((Model) right.getModel()).clear();
436: }
437:
438: /**
439: * Add all elements from the specified collection into the list on the left side.
440: * Elements are sorted if {@link #isAutoSortEnabled} returns {@code true}.
441: *
442: * @param items Items to add.
443: */
444: public void addElements(final Collection items) {
445: addElements(items.toArray());
446: }
447:
448: /**
449: * Add all elements from the specified array into the list on the left side.
450: * Elements are sorted if {@link #isAutoSortEnabled} returns {@code true}.
451: *
452: * @param items Items to add.
453: *
454: * @since 2.2
455: */
456: public void addElements(final Object[] items) {
457: Locale locale;
458: try {
459: locale = getLocale();
460: } catch (IllegalComponentStateException e) {
461: locale = getDefaultLocale();
462: }
463: final List list = new ArrayList(items.length);
464: for (int i = 0; i < items.length; i++) {
465: Object candidate = items[i];
466: if (!(candidate instanceof String)) {
467: candidate = new ListElement(candidate, locale);
468: }
469: list.add(candidate);
470: }
471: final Model left = (Model) this .left.getModel();
472: final Model right = (Model) this .right.getModel();
473: if (autoSort) {
474: list.addAll(left.choices);
475: Collections.sort(list);
476: left.clear();
477: right.clear();
478: }
479: left.addAll(list);
480: }
481:
482: /**
483: * Returns all elements with the specified selection state. If {@code selected} is {@code true},
484: * then this method returns the selected elements on the right side. If {@code selected} is
485: * {@code false}, then this method returns the unselected elements on the left side.
486: *
487: * @since 2.3
488: */
489: public Collection getElements(final boolean selected) {
490: return ((Model) (selected ? right : left).getModel())
491: .getElements();
492: }
493:
494: /**
495: * Add the specified elements to the selection list (the one to appears on the right side). If
496: * an element specified in the {@code selected} collection has not been previously {@linkplain
497: * #addElements(Collection) added}, it will be ignored.
498: *
499: * @since 2.3
500: */
501: public void selectElements(final Collection selected) {
502: final Model source = (Model) left.getModel();
503: final Model target = (Model) right.getModel();
504: int[] indices = new int[Math.min(selected.size(),
505: source.choices.size())];
506: int indice = 0, count = 0;
507: for (final Iterator it = source.choices.iterator(); it
508: .hasNext(); indice++) {
509: if (selected.contains(ListElement.unwrap(it.next()))) {
510: indices[count++] = indice;
511: }
512: }
513: indices = XArray.resize(indices, count);
514: target.move(source, indices);
515: }
516:
517: /**
518: * Set the font for both lists on the left and right side.
519: */
520: public void setFont(final Font font) {
521: // Note: 'left' and 'right' may be null during JComponent initialisation.
522: if (left != null)
523: left.setFont(font);
524: if (right != null)
525: right.setFont(font);
526: super .setFont(font);
527: }
528:
529: /**
530: * Display this component in a dialog box and wait for the user to press "Ok".
531: * This method can be invoked from any thread.
532: *
533: * @param owner The owner (may be null).
534: * @param title The title to write in the window bar.
535: * @return {@code true} if the user pressed "okay", or {@code false} otherwise.
536: *
537: * @since 2.2
538: */
539: public boolean showDialog(final Component owner, final String title) {
540: return SwingUtilities.showOptionDialog(owner, this , title);
541: }
542:
543: /**
544: * Show the dialog box. This method is provided only as an easy
545: * way to test the dialog appearance from the command line.
546: *
547: * @since 2.2
548: */
549: public static void main(final String[] args) {
550: final DisjointLists list = new DisjointLists();
551: list.addElements(Locale.getAvailableLocales());
552: list.showDialog(null, Utilities.getShortClassName(list));
553: System.out.println(list.getElements(true));
554: }
555: }
|