/*
* Geotoolkit.org - An Open Source Java GIS Toolkit
* http://www.geotoolkit.org
*
* (C) 2002-2012, Open Source Geospatial Foundation (OSGeo)
* (C) 2009-2012, Geomatys
*
* This library is free software; you can redistribute it and/or
* modify it under the terms of the GNU Lesser General Public
* License as published by the Free Software Foundation;
* version 2.1 of the License.
*
* This library is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
* Lesser General Public License for more details.
*/
package org.geotoolkit.gui.swing;
import java.awt.Font;
import java.awt.Dimension;
import java.awt.Component;
import java.awt.GridBagLayout;
import java.awt.GridBagConstraints;
import java.awt.event.ActionEvent;
import java.awt.event.ActionListener;
import java.awt.IllegalComponentStateException;
import javax.swing.JList;
import javax.swing.JButton;
import javax.swing.JComponent;
import javax.swing.JScrollPane;
import javax.swing.AbstractListModel;
import java.util.List;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.Arrays;
import java.util.Locale;
import java.text.ParseException;
import org.apache.sis.util.ArraysExt;
import org.geotoolkit.resources.Widgets;
import org.geotoolkit.internal.swing.SwingUtilities;
import static java.awt.GridBagConstraints.*;
/**
* A widget showing selected and unselected items in two disjoint lists. The list on the left
* side shows items available for selection. The list on the right side shows items already
* selected. User can move items from one list to the other using buttons in the middle.
*
* @author Martin Desruisseaux (IRD)
* @version 3.03
*
* @since 2.0
* @module
*/
@SuppressWarnings("serial")
public class DisjointLists extends JComponent implements Dialog {
/**
* The list model. Each {@link DisjointLists} object will use two instances
* of this class. Both instances share the same list of elements, but have
* their own list of index of visibles elements.
*/
@SuppressWarnings("serial")
private static final class Model extends AbstractListModel<Object> {
/**
* The list of elements shared by both lists. Not all elements in this list will be
* displayed. The index of elements to shown are enumerated in the {@link #visibles}
* array.
* <p>
* Note: this list is read by {@link DisjointLists#selectElements}. The content
* of this list should never be modified from any method outside this class.
*/
final List<Object> choices;
/**
* The index of valids elements in the {@link #choice} list. This array will growth
* as needed. Elements in this array should always be in strictly increasing order.
*/
private int[] visibles = new int[12];
/**
* The number of valid elements in the {@link #visibles} array.
*/
private int size;
/**
* Constructs a model for the specified list of elements.
*/
public Model(final List<Object> choices) {
this.choices = choices;
}
/**
* Returns {@code true} if all elements in the {@link #visible} array
* are in strictly increasing order. This is used for assertions.
*/
private boolean isSorted() {
for (int i=1; i<size; i++) {
if (visibles[i] <= visibles[i-1]) {
return false;
}
}
return true;
}
/**
* Searches the insertion point. This method returns always a positive value such that
* {@code value <= visibles[i]}. Note that the returned index may be {@link #size} if
* the given value is greater than the last {@link #visibles} value.
*/
private int search(final int lower, final int upper, final int value) {
int i = Arrays.binarySearch(visibles, lower, upper, value);
if (i < 0) i = ~i;
return i;
}
/**
* Returns the number of valid elements.
*/
@Override
public int getSize() {
assert size >= 0 && size <= choices.size() : size;
return size;
}
/**
* Returns all elements in this list.
*/
public Collection<Object> getElements() {
final Object[] list = new Object[getSize()];
for (int i=0; i<list.length; i++) {
list[i] = ListElement.unwrap(getElementAt(i));
}
return Arrays.asList(list);
}
/**
* Returns the element at the specified index.
*/
@Override
public Object getElementAt(final int index) {
assert index >= 0 && index < size : index;
return choices.get(visibles[index]);
}
/**
* Makes sure that the {@link #visibles} array has the specified capacity.
*/
private void ensureCapacity(final int capacity) {
if (visibles.length < capacity) {
visibles = Arrays.copyOf(visibles, Math.max(size*2, capacity));
}
}
/**
* Removes a range of visible elements. The {@code lower} and {@code upper}
* indices are index (not values) in the {@link #visibles} array.
*/
private void hide(final int lower, final int upper) {
if (lower != upper) {
System.arraycopy(visibles, upper, visibles, lower, size-upper);
size -= (upper-lower);
fireIntervalRemoved(this, lower, upper-1);
}
assert isSorted();
}
/**
* Moves elements in the specified range from the specified model to this model.
*
* @param source The source model.
* @param lower Lower index (inclusive) in the source model.
* @param upper Upper index (exclusive) in the source model.
*/
public void move(final Model source, final int lower, final int upper) {
assert lower >= 0 && upper <= source.size;
ensureCapacity(size + (upper - lower));
int insertAt = 0;
int subUpper = lower;
while (subUpper < upper) {
final int subLower = subUpper;
assert isSorted();
insertAt = search(insertAt, size, source.visibles[subLower]);
if (insertAt == size) {
subUpper = upper;
} else {
subUpper = source.search(subLower, upper, visibles[insertAt]);
}
final int length = subUpper - subLower;
System.arraycopy(visibles, insertAt, visibles, insertAt+length, size-insertAt);
System.arraycopy(source.visibles, subLower, visibles, insertAt, length);
size += length;
assert isSorted();
fireIntervalAdded(this, insertAt, insertAt+length-1);
}
source.hide(lower, upper);
}
/**
* Moves elements at the specified indices from the specified model to this model.
* Note: the indices array will be overwritten.
*
* @param source The source model.
* @param indices Indices of elements in the source model to move.
*/
public void move(final Model source, final int[] indices) {
Arrays.sort(indices);
for (int i=0; i<indices.length;) {
int lower = indices[i];
int upper = lower+1;
while (++i<indices.length && indices[i]==upper) {
// Collapses consecutive indices in a single move operation.
upper++;
}
move(source, lower, upper);
final int length = (upper-lower);
for (int j=i; j<indices.length; j++) {
// Adjusts the remaining indices. Since we just moved previous
// elements, the indices of remaining elements are shifted.
indices[j] -= length;
}
}
}
/**
* Adds all elements from the specified collection.
*/
public void addAll(final Collection<?> items) {
if (!items.isEmpty()) {
choices.addAll(items);
final int length = items.size();
ensureCapacity(size + length);
final int max = choices.size();
for (int i=max-length; i<max; i++) {
visibles[size++] = i;
}
assert isSorted();
fireIntervalAdded(this, size-length, size-1);
}
}
/**
* Removes all elements from this model.
*/
public void clear() {
choices.clear();
if (size != 0) {
final int oldSize = size;
size = 0;
fireIntervalRemoved(this, 0, oldSize-1);
}
}
}
/**
* Action invoked when the user pressed a button. This action
* invokes {@link Model#move} with selected indices.
*/
private static final class Action implements ActionListener {
/**
* The source and target lists.
*/
private final JList<Object> source, target;
/**
* {@code true} if we should move all items on action.
*/
private final boolean all;
/**
* Constructs a new "move" action.
*/
public Action(final JList<Object> source, final JList<Object> target, final boolean all) {
this.source = source;
this.target = target;
this.all = all;
}
/**
* Invoked when the user pressed a "move" button.
*/
@Override
public void actionPerformed(final ActionEvent event) {
final Model source = (Model) this.source.getModel();
final Model target = (Model) this.target.getModel();
if (all) {
target.move(source, 0, source.getSize());
return;
}
final int[] indices = this.source.getSelectedIndices();
target.move(source, indices);
}
}
/**
* The list on the left side. This is the list that contains
* the element selectable by the user.
*/
private final JList<Object> left;
/**
* The list on the right side. This list is initially empty.
*/
private final JList<Object> right;
/**
* {@code true} if elements should be automatically sorted.
*/
private boolean autoSort = true;
/**
* Constructs a new, initially empty, list.
*/
public DisjointLists() {
setLayout(new GridBagLayout());
/*
* Setup lists
*/
final List<Object> choices = new ArrayList<>();
left = new JList<>(new Model(choices));
right = new JList<>(new Model(choices));
final JScrollPane leftPane = new JScrollPane( left);
final JScrollPane rightPane = new JScrollPane(right);
final Dimension size = new Dimension(160, 200);
leftPane .setPreferredSize(size);
rightPane.setPreferredSize(size);
/*
* Setup buttons
*/
final JButton add = getButton("StepForward", ">", Widgets.format(Widgets.Keys.AddSelectedElements));
final JButton remove = getButton("StepBack", "<", Widgets.format(Widgets.Keys.RemoveSelectedElements));
final JButton addAll = getButton("FastForward", ">>", Widgets.format(Widgets.Keys.AddAll));
final JButton removeAll = getButton("Rewind", "<<", Widgets.format(Widgets.Keys.RemoveAll));
add .addActionListener(new Action(left, right, false));
remove .addActionListener(new Action(right, left, false));
addAll .addActionListener(new Action(left, right, true));
removeAll.addActionListener(new Action(right, left, true));
/*
* Build UI
*/
final GridBagConstraints c = new GridBagConstraints();
c.gridy=0; c.gridwidth=1; c.gridheight=4; c.weightx=c.weighty=1; c.fill=BOTH;
c.gridx=0; add( leftPane, c);
c.gridx=2; add(rightPane, c);
c.insets.left = c.insets.right = 9;
c.gridx=1; c.gridheight=1; c.weightx=0; c.fill=HORIZONTAL;
c.gridy=0; c.anchor=SOUTH; add(add, c);
c.gridy=3; c.anchor=NORTH; add(removeAll, c);
c.gridy=2; c.weighty=0; add(addAll, c);
c.gridy=1; c.insets.bottom=9; add(remove, c);
}
/**
* Returns a button.
*
* @param loader The class loader for loading the button's image.
* @param image The image name to load in the "media" category from the
* <A HREF="http://developer.java.sun.com/developer/techDocs/hi/repository/">Swing
* graphics repository</A>.
* @param fallback The fallback to use if the image is not found.
* @param description a brief description to use for tooltips.
* @return The button.
*/
private static JButton getButton(String image, final String fallback, final String description) {
image = "toolbarButtonGraphics/media/" + image + "16.gif";
return IconFactory.DEFAULT.getButton(image, description, fallback);
}
/**
* Returns {@code true} if elements are automatically sorted when added to a list.
* This applies to both lists (selected and unselected items). The default value is
* {@code true}.
*
* @return {@code true} if list elements are sorted.
*
* @since 2.2
*/
public boolean isAutoSortEnabled() {
return autoSort;
}
/**
* Sets to {@code true} if elements should be automatically sorted when added to a list.
* This applies to both lists (selected and unselected items).
*
* @param autoSort {@code true} if list elements should be sorted.
*
* @since 2.2
*/
public void setAutoSortEnabled(final boolean autoSort) {
if (autoSort != this.autoSort) {
this.autoSort = autoSort;
if (autoSort) {
final List<Object> elements = new ArrayList<>(((Model) left.getModel()).choices);
clear();
addElements(elements);
}
firePropertyChange("autoSort", !autoSort, autoSort);
}
}
/**
* Removes all elements from this list.
*
* @since 2.2
*/
public void clear() {
((Model) left .getModel()).clear();
((Model) right.getModel()).clear();
}
/**
* Adds all elements from the specified collection into the list of unselected elements (on
* the widget left side). Elements are sorted if {@link #isAutoSortEnabled} returns {@code true}.
*
* @param items Items to add.
*/
public void addElements(final Collection<?> items) {
addElements(items.toArray());
}
/**
* Adds all elements from the specified array into the list of unselected element (on the
* widget left side). Elements are sorted if {@link #isAutoSortEnabled} returns {@code true}.
*
* @param items Items to add.
*
* @since 2.2
*/
@SuppressWarnings({"unchecked","rawtypes"})
public void addElements(final Object[] items) {
Locale locale;
try {
locale = getLocale();
} catch (IllegalComponentStateException e) {
locale = getDefaultLocale();
}
final List<Object> list = new ArrayList<>(items.length);
for (int i=0; i<items.length; i++) {
Object candidate = items[i];
if (!(candidate instanceof String)) {
candidate = new ListElement(candidate, locale);
}
list.add(candidate);
}
final Model left = (Model) this.left .getModel();
final Model right = (Model) this.right.getModel();
if (autoSort) {
list.addAll(left.choices);
Collections.sort((List) list);
left .clear();
right.clear();
}
left.addAll(list);
}
/**
* Returns all elements with the specified selection state. If {@code selected} is {@code true},
* then this method returns the selected elements that are listed on the right side of the
* widget. If {@code selected} is {@code false}, then this method returns the unselected
* elements isted on the left side of the widget.
*
* @param selected {@code true} for fetching selected elements, or {@code false} for fetching
* unselected ones.
* @return The elements in the specified selection state.
*
* @since 2.3
*/
public Collection<Object> getElements(final boolean selected) {
return ((Model) (selected ? right : left).getModel()).getElements();
}
/**
* Adds the specified elements to the selection list (the one that are listed on the right side
* of the widget). If an element specified in the {@code selected} collection has not been
* previously {@linkplain #addElements(Collection) added}, it will be ignored.
*
* @param selected The elements to add to list of selected elements.
*
* @since 2.3
*/
public void selectElements(final Collection<?> selected) {
final Model source = (Model) left .getModel();
final Model target = (Model) right.getModel();
int[] indices = new int[Math.min(selected.size(), source.choices.size())];
int indice=0, count=0;
for (final Object choice : source.choices) {
if (selected.contains(ListElement.unwrap(choice))) {
indices[count++] = indice;
}
indice++;
}
indices = ArraysExt.resize(indices, count);
target.move(source, indices);
}
/**
* Sets the font for both lists (selected and unselected elements) which appear on
* the left and right sides of the widget.
*/
@Override
public void setFont(final Font font) {
// Note: 'left' and 'right' may be null during JComponent initialisation.
if (left != null) left .setFont(font);
if (right != null) right.setFont(font);
super.setFont(font);
}
/**
* {@inheritDoc}
*
* @since 3.12
*/
@Override
public void commitEdit() throws ParseException {
}
/**
* {@inheritDoc}
*
* @since 2.2
*/
@Override
public boolean showDialog(final Component owner, final String title) {
return SwingUtilities.showDialog(owner, this, title);
}
}