/* * Geotoolkit.org - An Open Source Java GIS Toolkit * http://www.geotoolkit.org * * (C) 2009-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.util.Map; import java.util.HashMap; import java.util.List; import java.util.Arrays; import java.util.ArrayList; import java.util.Collection; import java.util.ListIterator; import java.lang.reflect.Array; import javax.swing.table.AbstractTableModel; import org.apache.sis.util.ArraysExt; import org.geotoolkit.internal.swing.SwingUtilities; /** * A {@linkplain javax.swing.table.TableModel table model} backed by a {@linkplain List list}. The * list is given to the constructor and retained by direct reference - it is <strong>not</strong> * cloned, because it may contain millions of elements, sometime through a custom {@link List} * implementation fetching the information on-the-fly from a database. Consequently if the content * of the list is modified externally, then a {@code fireXXX} method (inherited from * {@link AbstractTableModel} must be invoked explicitly. Note that those {@code fireXXX} methods * don't need to be invoked if the list is modified through the methods provided in this class. * * {@section Multi-threading} * Unless otherwise specified, methods in this class must be invoked from the Swing thread. * Exceptions are the {@code getElements} method, which will be automatically executed in * the Swing thread no matter the invoker thread. This convenience does <strong>not</strong> * apply to methods inherited from the super-class, and may not apply to methods defined in * the sub-classes. * * {@section Serialization} * This model is serialiable if the underlying list is serializable. * * @param <E> The type of elements in the list backing this table model. * * @author Martin Desruisseaux (Geomatys) * @version 3.13 * * @since 3.00 * @module */ public abstract class ListTableModel<E> extends AbstractTableModel { /** * Serial number for compatibility with different versions. */ private static final long serialVersionUID = 3543567151179489778L; /** * The type of elements. */ private final Class<E> type; /** * The list of row elements. This is a direct reference to the list given by the user at * construction time, not a clone. If this list is modified externaly, then the appropriate * {@code fireXXX} method must be invoked explicitly. */ protected final List<E> elements; /** * Creates a new table model backed by an {@link ArrayList}. * * @param type The type of elements in the list. */ protected ListTableModel(final Class<E> type) { this.type = type; elements = new ArrayList<>(); } /** * Creates a new table model for the given list. The given list is retained by direct * reference - it is not cloned. Consequently if the content of this list is modified * externaly, then one of the {@code fireXXX} method inherited from {@link AbstractTableModel} * must be invoked explicitly. * * @param type The type of elements in the list. * @param elements The list of elements to display in a table. */ protected ListTableModel(final Class<E> type, final List<E> elements) { this.type = type; this.elements = elements; } /** * Returns the number of rows in the table. * * @return The number of rows. */ @Override public int getRowCount() { return elements.size(); } /** * Creates an array of the given length. */ @SuppressWarnings("unchecked") private E[] createArray(final int length) { return (E[]) Array.newInstance(type, length); } /** * For internal use by {@link ListTableModel#getElements}: asks for the elements from * the Swing thread. The elements can be asked in three different ways. Only one of * those ways can be used at a time. */ private final class Get implements Runnable { /** The result to be returned. */ E[] array; /** One possible way to query. */ boolean all; /** One possible way to query. */ int lower, upper; /** One possible way to query. */ int[] selection; @Override public void run() { List<E> elements = ListTableModel.this.elements; int[] selection = this.selection; if (selection != null) { array = createArray(selection.length); for (int i=0; i<selection.length; i++) { array[i] = elements.get(selection[i]); } return; } else if (!all) { elements = elements.subList(lower, upper); } array = elements.toArray(createArray(elements.size())); } } /** * Returns a snapshot of every elements contained in this model. This method * will be executed in the Swing thread even if invoked from an other thread. * * @return A snapshot of every elements in this model. */ public E[] getElements() { final Get task = new Get(); task.all = true; SwingUtilities.invokeAndWait(task); return task.array; } /** * Returns a snapshot of a range of elements contained in this model. This method * will be executed in the Swing thread even if invoked from an other thread. * * @param lower Index of the first element to be returned. * @param upper Index after the last element to be returned. * @return A snapshot of the given range of elements in this model. */ public E[] getElements(final int lower, final int upper) { final Get task = new Get(); task.lower = lower; task.upper = upper; SwingUtilities.invokeAndWait(task); return task.array; } /** * Returns a snapshot of selected elements. This method will be executed * in the Swing thread even if invoked from an other thread. * * @param selection The indices of selected elements, typically given by * {@link javax.swing.JTable#getSelectedRows()}. * @return A snapshot of the selected elements in this model. */ public E[] getElements(final int[] selection) { final Get task = new Get(); task.selection = selection; SwingUtilities.invokeAndWait(task); return task.array; } /** * Sets the elements. All previous elements are discarded before to add the new ones. * * @param elts The new elements. * * @since 3.13 */ public void setElements(final E... elts) { elements.clear(); elements.addAll(Arrays.asList(elts)); fireTableDataChanged(); } /** * Adds all elements from the given collection. The default implementation invokes * {@link List#addAll(Collection)} and uses the change of {@linkplain List#size list size} * for computing the index to be given to the {@link #fireTableRowsInserted(int,int)} method. * Consequently the list implementation doesn't need to accept every elements. Note however * that the list must not change in a concurrent thread, otherwise the change event to be * fired may have an inacurate index range. * * @param toAdd The elements to add. * @throws UnsupportedOperationException if the underlying {@linkplain #elements} list * is not modifiable. */ public void add(final Collection<? extends E> toAdd) throws UnsupportedOperationException { if (!toAdd.isEmpty()) { final int insertAt = elements.size(); if (elements.addAll(toAdd)) { final int last = elements.size() - 1; if (last >= insertAt) { fireTableRowsInserted(insertAt, last); } } } } /** * Inserts at the given position all elements from the given collection. The elements in this * table that are after the insertion point are shifted to larger index. * <p> * The default implementation invokes {@link List#addAll(int,Collection)} and uses the * change of {@linkplain List#size list size} for computing the index to be given to the * {@link #fireTableRowsInserted(int,int)} method. Consequently the list implementation * doesn't need to accept every elements. Note however that the list must not change in * a concurrent thread, otherwise the change event to be fired may have an inacurate index * range. * * @param insertAt The insertion point. The first element will be inserted at that position. * @param toAdd The elements to add. * @throws UnsupportedOperationException if the underlying {@linkplain #elements} list * is not modifiable. * @throws IndexOutOfBoundsException if the given index is out of range. */ public void insert(final int insertAt, final Collection<? extends E> toAdd) throws UnsupportedOperationException, IndexOutOfBoundsException { if (!toAdd.isEmpty()) { int count = elements.size(); if (elements.addAll(insertAt, toAdd)) { count = elements.size() - count; if (count > 0) { fireTableRowsInserted(insertAt, insertAt + count - 1); } } } } /** * Removes a range of rows (or elements). Note that the upper value is inclusive, * which is different than the <cite>Java Collection</cite> usage but consistent * with the <cite>Swing</cite> usage. * * @param lower Index of the first row to remove, inclusive. * @param upper Index of the last row to remove, <strong>inclusive</strong>. * @throws UnsupportedOperationException if the underlying {@linkplain #elements} list * is not modifiable. */ public void remove(final int lower, final int upper) throws UnsupportedOperationException { if (lower == upper) { elements.remove(lower); } else { elements.subList(lower, upper+1).clear(); } fireTableRowsDeleted(lower, upper); } /** * Removes the given elements from the list. The default implementation delegates the work * to {@link #remove(int,int)}. Consecutive indexes will be removed in a single call of the * above method. * * @param indices The index of elements to remove. * @throws UnsupportedOperationException if the underlying {@linkplain #elements} list * is not modifiable. */ public void remove(int[] indices) throws UnsupportedOperationException { // We must iterate in reverse order, because the // index after the removed elements will change. int i = indices.length; if (i != 0) { if (!ArraysExt.isSorted(indices, false)) { indices = indices.clone(); Arrays.sort(indices); } int upper = indices[--i]; int lower = upper; while (i != 0) { int previous = indices[--i]; if (previous != lower - 1) { remove(lower, upper); // Reminder: upper is inclusive upper = previous; } lower = previous; } remove(lower, upper); // Reminder: upper is inclusive } } /** * Removes duplicated elements. If two elements are equal in the sense of the * {@link Object#equals(Object)} method, then the first one is removed from the * {@linkplain #elements} list. We keep the last element instead of the first one * on the assumption that the last element is the most recently added. * * @return The number of duplicates which have been found. * @throws UnsupportedOperationException if the underlying {@linkplain #elements} list * is not modifiable. */ public int removeDuplicates() throws UnsupportedOperationException { int count = 0; final int[] indices = new int[elements.size()]; final Map<E,Integer> previous = new HashMap<>(); final ListIterator<E> it = elements.listIterator(); while (it.hasNext()) { E element = it.next(); final Integer p = previous.put(element, it.previousIndex()); if (p != null) { indices[count++] = p; } } if (count != 0) { remove(ArraysExt.resize(indices, count)); } return count; } /** * Sorts the elements in their natural order. * * @return {@code true} if the row order changed as a result of this method call. * @throws UnsupportedOperationException if the underlying {@linkplain #elements} list * is not modifiable. * @throws ClassCastException if the list contains elements that are not * <cite>mutually comparable</cite>. */ public boolean sort() throws UnsupportedOperationException, ClassCastException { boolean changed = false; if (!elements.isEmpty()) { final E[] array = elements.toArray(createArray(elements.size())); Arrays.sort(array); final ListIterator<E> it = elements.listIterator(); for (int i=0; i<array.length; i++) { final E t = array[i]; if (it.next() != t) { it.set(t); changed = true; } } if (changed) { fireTableRowsUpdated(0, elements.size() - 1); } } return changed; } }