--- /dev/null
+package be.nikiroo.utils.ui;
+
+import java.awt.Component;
+import java.awt.Point;
+import java.awt.event.MouseAdapter;
+import java.awt.event.MouseEvent;
+import java.util.ArrayList;
+import java.util.Collection;
+import java.util.List;
+
+import javax.swing.JList;
+import javax.swing.JPopupMenu;
+import javax.swing.ListCellRenderer;
+
+import be.nikiroo.utils.compat.DefaultListModel6;
+import be.nikiroo.utils.compat.JList6;
+import be.nikiroo.utils.compat.ListCellRenderer6;
+
+/**
+ * A {@link javax.swing.ListModel} that can maintain 2 lists; one with the
+ * actual data (the elements), and a second one with the items that are
+ * currently displayed (the items).
+ * <p>
+ * It also offers filter options, supports hovered changes and some more utility
+ * functions.
+ *
+ * @author niki
+ *
+ * @param <T>
+ * the type of elements and items (the same type)
+ */
+public class ListModel<T> extends DefaultListModel6<T> {
+ private static final long serialVersionUID = 1L;
+
+ /**
+ * A filter interface, to check for a condition (note that a Predicate class
+ * already exists in Java 1.8+, and is compatible with this one if you
+ * change the signatures -- but I support java 1.6+).
+ *
+ * @author niki
+ *
+ * @param <T>
+ * the type of elements and items (the same type)
+ */
+ public interface Predicate<T> {
+ /**
+ * Check if an item or an element pass a filter.
+ *
+ * @param item
+ * the item to test
+ *
+ * @return TRUE if the test passed, FALSE if not
+ */
+ public boolean test(T item);
+ }
+
+ /**
+ * A simple interface your elements must implement if you want to use
+ * {@link ListModel#generateRenderer(ListModel)}.
+ *
+ * @author niki
+ */
+ public interface Hoverable {
+ /**
+ * The element is currently selected.
+ *
+ * @param selected
+ * TRUE for selected, FALSE for unselected
+ */
+ public void setSelected(boolean selected);
+
+ /**
+ * The element is currently under the mouse cursor.
+ *
+ * @param hovered
+ * TRUE if it is, FALSE if not
+ */
+ public void setHovered(boolean hovered);
+ }
+
+ private int hoveredIndex;
+ private List<T> items = new ArrayList<T>();
+ private JList6<T> list;
+
+ /**
+ * Create a new {@link ListModel}.
+ *
+ * @param list
+ * the {@link JList} we will handle the data of (cannot be NULL)
+ */
+ @SuppressWarnings({ "unchecked", "rawtypes" }) // not compatible Java 1.6
+ public ListModel(JList list) {
+ this((JList6<T>) list);
+ }
+
+ /**
+ * Create a new {@link ListModel}.
+ *
+ * @param list
+ * the {@link JList} we will handle the data of (cannot be NULL)
+ * @param popup
+ * the popup to use and keep track of (can be NULL)
+ */
+ @SuppressWarnings({ "unchecked", "rawtypes" }) // not compatible Java 1.6
+ public ListModel(final JList list, final JPopupMenu popup) {
+ this((JList6<T>) list, popup);
+ }
+
+ /**
+ * Create a new {@link ListModel}.
+ *
+ * @param list
+ * the {@link JList6} we will handle the data of (cannot be NULL)
+ */
+ public ListModel(JList6<T> list) {
+ this(list, null);
+ }
+
+ /**
+ * Create a new {@link ListModel}.
+ *
+ * @param list
+ * the {@link JList6} we will handle the data of (cannot be NULL)
+ * @param popup
+ * the popup to use and keep track of (can be NULL)
+ */
+ public ListModel(final JList6<T> list, final JPopupMenu popup) {
+ this.list = list;
+ list.setModel(this);
+
+ list.addMouseMotionListener(new MouseAdapter() {
+ @Override
+ public void mouseMoved(MouseEvent me) {
+ if (popup != null && popup.isShowing())
+ return;
+
+ Point p = new Point(me.getX(), me.getY());
+ int index = list.locationToIndex(p);
+ if (index != hoveredIndex) {
+ int oldIndex = hoveredIndex;
+ hoveredIndex = index;
+ fireElementChanged(oldIndex);
+ fireElementChanged(index);
+ }
+ }
+ });
+
+ list.addMouseListener(new MouseAdapter() {
+ @Override
+ public void mousePressed(MouseEvent e) {
+ check(e);
+ }
+
+ @Override
+ public void mouseReleased(MouseEvent e) {
+ check(e);
+ }
+
+ @Override
+ public void mouseExited(MouseEvent e) {
+ if (popup != null && popup.isShowing())
+ return;
+
+ if (hoveredIndex > -1) {
+ int oldIndex = hoveredIndex;
+ hoveredIndex = -1;
+ fireElementChanged(oldIndex);
+ }
+ }
+
+ private void check(MouseEvent e) {
+ if (popup == null) {
+ return;
+ }
+
+ if (e.isPopupTrigger()) {
+ if (list.getSelectedIndices().length <= 1) {
+ list.setSelectedIndex(
+ list.locationToIndex(e.getPoint()));
+ }
+
+ popup.show(list, e.getX(), e.getY());
+ }
+ }
+ });
+ }
+
+ /**
+ * Check if this element is currently under the mouse.
+ *
+ * @param element
+ * the element to check
+ *
+ * @return TRUE if it is
+ */
+ public boolean isHovered(T element) {
+ return indexOf(element) == hoveredIndex;
+ }
+
+ /**
+ * Check if this element is currently under the mouse.
+ *
+ * @param index
+ * the index of the element to check
+ *
+ * @return TRUE if it is
+ */
+ public boolean isHovered(int index) {
+ return index == hoveredIndex;
+ }
+
+ /**
+ * Add an item to the model.
+ *
+ * @param item
+ * the new item to add
+ */
+ public void addItem(T item) {
+ items.add(item);
+ }
+
+ /**
+ * Add items to the model.
+ *
+ * @param items
+ * the new items to add
+ */
+ public void addAllItems(Collection<T> items) {
+ this.items.addAll(items);
+ }
+
+ /**
+ * Removes the first occurrence of the specified element from this list, if
+ * it is present (optional operation).
+ *
+ * @param item
+ * the item to remove if possible (can be NULL)
+ *
+ * @return TRUE if one element was removed, FALSE if not found
+ */
+ public boolean removeItem(T item) {
+ return items.remove(item);
+ }
+
+ /**
+ * Remove the items that pass the given filter (or all items if the filter
+ * is NULL).
+ *
+ * @param filter
+ * the filter (if the filter returns TRUE, the item will be
+ * removed)
+ *
+ * @return TRUE if at least one item was removed
+ */
+ public boolean removeItemIf(Predicate<T> filter) {
+ boolean changed = false;
+ if (filter == null) {
+ changed = !items.isEmpty();
+ clearItems();
+ } else {
+ for (int i = 0; i < items.size(); i++) {
+ if (filter.test(items.get(i))) {
+ items.remove(i--);
+ changed = true;
+ }
+ }
+ }
+
+ return changed;
+ }
+
+ /**
+ * Removes all the items from this model.
+ */
+ public void clearItems() {
+ items.clear();
+ }
+
+ /**
+ * Filter the current elements.
+ * <p>
+ * This method will clear all the elements then look into all the items:
+ * those that pass the given filter will be copied as elements.
+ *
+ * @param filter
+ * the filter to select which elements to keep; an item that pass
+ * the filter will be copied as an element (can be NULL, in that
+ * case all items will be copied as elements)
+ */
+ @SuppressWarnings("unchecked") // ListModel<T> and JList<T> are not java 1.6
+ public void filter(Predicate<T> filter) {
+ clear();
+ for (T item : items) {
+ if (filter == null || filter.test(item)) {
+ addElement(item);
+ }
+ }
+
+ list.repaint();
+ }
+
+ /**
+ * Return the currently selected elements.
+ *
+ * @return the selected elements
+ */
+ public List<T> getSelectedElements() {
+ List<T> selected = new ArrayList<T>();
+ for (int index : list.getSelectedIndices()) {
+ selected.add(get(index));
+ }
+
+ return selected;
+ }
+
+ /**
+ * Return the selected element if <b>one</b> and <b>only one</b> element is
+ * selected. I.E., if zero, two or more elements are selected, NULL will be
+ * returned.
+ *
+ * @return the element if it is the only selected element, NULL otherwise
+ */
+ public T getUniqueSelectedElement() {
+ List<T> selected = getSelectedElements();
+ if (selected.size() == 1) {
+ return selected.get(0);
+ }
+
+ return null;
+ }
+
+ /**
+ * Notify that this element has been changed.
+ *
+ * @param index
+ * the index of the element
+ */
+ public void fireElementChanged(int index) {
+ if (index >= 0) {
+ fireContentsChanged(this, index, index);
+ }
+ }
+
+ /**
+ * Notify that this element has been changed.
+ *
+ * @param element
+ * the element
+ */
+ public void fireElementChanged(T element) {
+ int index = indexOf(element);
+ if (index >= 0) {
+ fireContentsChanged(this, index, index);
+ }
+ }
+
+ @SuppressWarnings("unchecked") // ListModel<T> and JList<T> are not java 1.6
+ @Override
+ public T get(int index) {
+ return (T) super.get(index);
+ }
+
+ /**
+ * Generate a {@link ListCellRenderer} that supports {@link Hoverable}
+ * elements.
+ *
+ * @param <T>
+ * the type of elements and items (the same type), which should
+ * implement {@link Hoverable} (it will not cause issues if not,
+ * but then, it will be a default renderer)
+ * @param model
+ * the model to use
+ *
+ * @return a suitable, {@link Hoverable} compatible renderer
+ */
+ static public <T extends Component> ListCellRenderer6<T> generateRenderer(
+ final ListModel<T> model) {
+ return new ListCellRenderer6<T>() {
+ @Override
+ public Component getListCellRendererComponent(JList6<T> list,
+ T item, int index, boolean isSelected,
+ boolean cellHasFocus) {
+ if (item instanceof Hoverable) {
+ Hoverable hoverable = (Hoverable) item;
+ hoverable.setSelected(isSelected);
+ hoverable.setHovered(model.isHovered(index));
+ }
+
+ return item;
+ }
+ };
+ }
+}
--- /dev/null
+/*
+ * This program 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, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program 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.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ */
+// Can be found at: https://code.google.com/archive/p/aephyr/source/default/source
+// package aephyr.swing;
+package be.nikiroo.utils.ui;
+
+import java.text.Collator;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.Collection;
+import java.util.Collections;
+import java.util.Comparator;
+import java.util.Enumeration;
+import java.util.HashSet;
+import java.util.List;
+import java.util.Map;
+import java.util.HashMap;
+import java.util.Iterator;
+import java.util.regex.Matcher;
+import java.util.regex.Pattern;
+
+import javax.swing.JTree;
+import javax.swing.SortOrder;
+import javax.swing.event.EventListenerList;
+import javax.swing.event.TreeExpansionEvent;
+import javax.swing.event.TreeExpansionListener;
+import javax.swing.event.TreeModelEvent;
+import javax.swing.event.TreeModelListener;
+import javax.swing.event.TreeWillExpandListener;
+import javax.swing.tree.ExpandVetoException;
+import javax.swing.tree.TreeModel;
+import javax.swing.tree.TreePath;
+
+
+public class TreeModelTransformer<N> implements TreeModel {
+
+ public TreeModelTransformer(JTree tree, TreeModel model) {
+ if (tree == null)
+ throw new IllegalArgumentException();
+ if (model == null)
+ throw new IllegalArgumentException();
+ this.tree = tree;
+ this.model = model;
+ handler = createHandler();
+ addListeners();
+ }
+
+ private JTree tree;
+
+ private TreeModel model;
+
+ private Handler handler;
+
+ private Filter<N> filter;
+
+ private TreePath filterStartPath;
+
+ private int filterDepthLimit;
+
+ private SortOrder sortOrder = SortOrder.UNSORTED;
+
+ private Map<Object,Converter> converters;
+
+ protected EventListenerList listenerList = new EventListenerList();
+
+ protected Handler createHandler() {
+ return new Handler();
+ }
+
+ protected void addListeners() {
+ tree.addTreeExpansionListener(handler);
+ model.addTreeModelListener(handler);
+ }
+
+ protected void removeListeners() {
+ tree.removeTreeExpansionListener(handler);
+ model.removeTreeModelListener(handler);
+ }
+
+ public void dispose() {
+ removeListeners();
+ }
+
+ public TreeModel getModel() {
+ return model;
+ }
+
+ private Converter getConverter(Object node) {
+ return converters == null ? null : converters.get(node);
+ }
+
+ int convertRowIndexToView(Object parent, int index) {
+ Converter converter = getConverter(parent);
+ if (converter != null)
+ return converter.convertRowIndexToView(index);
+ return index;
+ }
+
+ int convertRowIndexToModel(Object parent, int index) {
+ Converter converter = getConverter(parent);
+ if (converter != null)
+ return converter.convertRowIndexToModel(index);
+ return index;
+ }
+
+ @Override
+ public Object getChild(Object parent, int index) {
+ return model.getChild(parent, convertRowIndexToModel(parent, index));
+ }
+
+ @Override
+ public int getChildCount(Object parent) {
+ Converter converter = getConverter(parent);
+ if (converter != null)
+ return converter.getChildCount();
+ return model.getChildCount(parent);
+ }
+
+ @Override
+ public int getIndexOfChild(Object parent, Object child) {
+ int index = model.getIndexOfChild(parent, child);
+ if (index < 0)
+ return -1;
+ return convertRowIndexToView(parent, index);
+ }
+
+ @Override
+ public Object getRoot() {
+ return model.getRoot();
+ }
+
+ @Override
+ public boolean isLeaf(Object node) {
+ return model.isLeaf(node);
+ }
+
+ @Override
+ public void valueForPathChanged(TreePath path, Object newValue) {
+ model.valueForPathChanged(path, newValue);
+ }
+
+ @Override
+ public void addTreeModelListener(TreeModelListener l) {
+ listenerList.add(TreeModelListener.class, l);
+ }
+
+ @Override
+ public void removeTreeModelListener(TreeModelListener l) {
+ listenerList.remove(TreeModelListener.class, l);
+ }
+
+ /**
+ * Set the comparator that compares nodes in sorting.
+ * @param comparator
+ * @see #getComparator()
+ */
+ public void setComparator(Comparator<N> comparator) {
+ handler.setComparator(comparator);
+ }
+
+ /**
+ * @return comparator that compares nodes
+ * @see #setComparator(Comparator)
+ */
+ public Comparator<N> getComparator() {
+ return handler.getComparator();
+ }
+
+ public void setSortOrder(SortOrder newOrder) {
+ SortOrder oldOrder = sortOrder;
+ if (oldOrder == newOrder)
+ return;
+ sortOrder = newOrder;
+ ArrayList<TreePath> paths = null;
+ switch (newOrder) {
+ case ASCENDING:
+ if (oldOrder == SortOrder.DESCENDING) {
+ flip();
+ } else {
+ paths = sort();
+ }
+ break;
+ case DESCENDING:
+ if (oldOrder == SortOrder.ASCENDING) {
+ flip();
+ } else {
+ paths = sort();
+ }
+ break;
+ case UNSORTED:
+ unsort();
+ break;
+ }
+ fireTreeStructureChangedAndExpand(new TreePath(getRoot()), paths, true);
+ }
+
+ public SortOrder getSortOrder() {
+ return sortOrder;
+ }
+
+ public void toggleSortOrder() {
+ setSortOrder(sortOrder == SortOrder.ASCENDING ?
+ SortOrder.DESCENDING : SortOrder.ASCENDING);
+ }
+
+
+ /**
+ * Flip all sorted paths.
+ */
+ private void flip() {
+ for (Converter c : converters.values()) {
+ flip(c.viewToModel);
+ }
+ }
+
+ /**
+ * Flip array.
+ * @param array
+ */
+ private static void flip(int[] array) {
+ for (int left=0, right=array.length-1;
+ left<right; left++, right--) {
+ int tmp = array[left];
+ array[left] = array[right];
+ array[right] = tmp;
+ }
+ }
+
+ private void unsort() {
+ if (filter == null) {
+ converters = null;
+ } else {
+ Iterator<Converter> cons = converters.values().iterator();
+ while (cons.hasNext()) {
+ Converter converter = cons.next();
+ if (!converter.isFiltered()) {
+ cons.remove();
+ } else {
+ Arrays.sort(converter.viewToModel);
+ }
+ }
+ }
+ }
+
+ /**
+ * Sort root and expanded descendants.
+ * @return list of paths that were sorted
+ */
+ private ArrayList<TreePath> sort() {
+ if (converters == null)
+ converters = createConvertersMap(); //new IdentityHashMap<Object,Converter>();
+ return sortHierarchy(new TreePath(model.getRoot()));
+ }
+
+ /**
+ * Sort path and expanded descendants.
+ * @param path
+ * @return list of paths that were sorted
+ */
+ private ArrayList<TreePath> sortHierarchy(TreePath path) {
+ ValueIndexPair<N>[] pairs = createValueIndexPairArray(20);
+ ArrayList<TreePath> list = new ArrayList<TreePath>();
+ pairs = sort(path.getLastPathComponent(), pairs);
+ list.add(path);
+ Enumeration<TreePath> paths = tree.getExpandedDescendants(path);
+ if (paths != null)
+ while (paths.hasMoreElements()) {
+ path = paths.nextElement();
+ pairs = sort(path.getLastPathComponent(), pairs);
+ list.add(path);
+ }
+ return list;
+ }
+
+ private ValueIndexPair<N>[] sort(Object node, ValueIndexPair<N>[] pairs) {
+ Converter converter = getConverter(node);
+ TreeModel mdl = model;
+ int[] vtm;
+ if (converter != null) {
+ vtm = converter.viewToModel;
+ if (pairs.length < vtm.length)
+ pairs = createValueIndexPairArray(vtm.length);
+ for (int i=vtm.length; --i>=0;) {
+ int idx = vtm[i];
+ pairs[i].index = idx;
+ pairs[i].value = (N)mdl.getChild(node, idx);
+ }
+ } else {
+ int count = mdl.getChildCount(node);
+ if (count <= 0)
+ return pairs;
+ if (pairs.length < count)
+ pairs = createValueIndexPairArray(count);
+ for (int i=count; --i>=0;) {
+ pairs[i].index = i;
+ pairs[i].value = (N)mdl.getChild(node, i);
+ }
+ vtm = new int[count];
+ }
+ Arrays.sort(pairs, 0, vtm.length, handler);
+ for (int i=vtm.length; --i>=0;)
+ vtm[i] = pairs[i].index;
+ if (converter == null) {
+ converters.put(node, new Converter(vtm, false));
+ }
+ if (sortOrder == SortOrder.DESCENDING)
+ flip(vtm);
+ return pairs;
+ }
+
+ private ValueIndexPair<N>[] createValueIndexPairArray(int len) {
+ ValueIndexPair<N>[] pairs = new ValueIndexPair[len];
+ for (int i=len; --i>=0;)
+ pairs[i] = new ValueIndexPair<N>();
+ return pairs;
+ }
+
+ public void setFilter(Filter<N> filter) {
+ setFilter(filter, null);
+ }
+
+ public void setFilter(Filter<N> filter, TreePath startingPath) {
+ setFilter(filter, null, -1);
+ }
+
+ public void setFilter(Filter<N> filter, TreePath startingPath, int depthLimit) {
+ if (filter == null && startingPath != null)
+ throw new IllegalArgumentException();
+ if (startingPath != null && startingPath.getPathCount() == 1)
+ startingPath = null;
+ Filter<N> oldFilter = this.filter;
+ TreePath oldStartPath = filterStartPath;
+ this.filter = filter;
+ filterStartPath = startingPath;
+ filterDepthLimit = depthLimit;
+ applyFilter(oldFilter, oldStartPath, null, true);
+ }
+
+ public Filter<N> getFilter() {
+ return filter;
+ }
+
+ public TreePath getFilterStartPath() {
+ return filterStartPath;
+ }
+
+ private void applyFilter(Filter<N> oldFilter, TreePath oldStartPath, Collection<TreePath> expanded, boolean sort) {
+ TreePath startingPath = filterStartPath;
+ ArrayList<TreePath> expand = null;
+ if (filter == null) {
+ converters = null;
+ } else {
+ if (converters == null || startingPath == null) {
+ converters = createConvertersMap();
+ } else if (oldFilter != null) {
+ // unfilter the oldStartPath if oldStartPath isn't descendant of startingPath
+ if (oldStartPath == null) {
+ converters = createConvertersMap();
+ fireTreeStructureChangedAndExpand(new TreePath(getRoot()), null, true);
+ } else if (!startingPath.isDescendant(oldStartPath)) {
+ Object node = oldStartPath.getLastPathComponent();
+ handler.removeConverter(getConverter(node), node);
+ fireTreeStructureChangedAndExpand(oldStartPath, null, true);
+ }
+ }
+ expand = new ArrayList<TreePath>();
+ TreePath path = startingPath != null ? startingPath : new TreePath(getRoot());
+ if (!applyFilter(filter, path, expand, filterDepthLimit)) {
+ converters.put(path.getLastPathComponent(), new Converter(Converter.EMPTY, true));
+ }
+ }
+ if (startingPath == null)
+ startingPath = new TreePath(getRoot());
+ fireTreeStructureChanged(startingPath);
+ if (expanded != null)
+ expand.retainAll(expanded);
+ expandPaths(expand);
+ if (sort && sortOrder != SortOrder.UNSORTED) {
+ if (filter == null)
+ converters = createConvertersMap();
+ if (startingPath.getPathCount() > 1 && oldFilter != null) {
+ // upgrade startingPath or sort oldStartPath
+ if (oldStartPath == null) {
+ startingPath = new TreePath(getRoot());
+ } else if (oldStartPath.isDescendant(startingPath)) {
+ startingPath = oldStartPath;
+ } else if (!startingPath.isDescendant(oldStartPath)) {
+ expand = sortHierarchy(oldStartPath);
+ fireTreeStructureChanged(oldStartPath);
+ expandPaths(expand);
+ }
+ }
+ expand = sortHierarchy(startingPath);
+ fireTreeStructureChanged(startingPath);
+ expandPaths(expand);
+ }
+
+ }
+
+ private boolean applyFilter(Filter<N> filter, TreePath path, ArrayList<TreePath> expand) {
+ int depthLimit = filterDepthLimit;
+ if (depthLimit >= 0) {
+ depthLimit -= filterStartPath.getPathCount() - path.getPathCount();
+ if (depthLimit <= 0)
+ return false;
+ }
+ return applyFilter(filter, path, expand, depthLimit);
+ }
+
+ private boolean applyFilter(Filter<N> filter, TreePath path, ArrayList<TreePath> expand, int depthLimit) {
+ Object node = path.getLastPathComponent();
+ int count = model.getChildCount(node);
+ int[] viewToModel = null;
+ int viewIndex = 0;
+ boolean needsExpand = false;
+ boolean isExpanded = false;
+ if (depthLimit > 0)
+ depthLimit--;
+ for (int i=0; i<count; i++) {
+ Object child = model.getChild(node, i);
+ boolean leaf = model.isLeaf(child);
+ if (filter.acceptNode(path, (N)child, leaf)) {
+ if (viewToModel == null)
+ viewToModel = new int[count-i];
+ viewToModel[viewIndex++] = i;
+ needsExpand = true;
+ } else if (depthLimit != 0 && !leaf) {
+ if (applyFilter(filter, path.pathByAddingChild(child), expand, depthLimit)) {
+ if (viewToModel == null)
+ viewToModel = new int[count-i];
+ viewToModel[viewIndex++] = i;
+ isExpanded = true;
+ }
+ }
+ }
+ if (needsExpand && expand != null && !isExpanded && path.getPathCount() > 1) {
+ expand.add(path);
+ }
+ if (viewToModel != null) {
+ if (viewIndex < viewToModel.length)
+ viewToModel = Arrays.copyOf(viewToModel, viewIndex);
+ // a node must have a converter to signify that tree modifications
+ // need to query the filter, so have to put in converter
+ // even if viewIndex == viewToModel.length
+ converters.put(node, new Converter(viewToModel, true));
+ return true;
+ }
+ return false;
+ }
+
+
+ private void expandPaths(ArrayList<TreePath> paths) {
+ if (paths == null || paths.isEmpty())
+ return;
+ JTree tre = tree;
+ for (TreePath path : paths)
+ tre.expandPath(path);
+ }
+
+
+ private void fireTreeStructureChangedAndExpand(TreePath path, ArrayList<TreePath> list, boolean retainSelection) {
+ Enumeration<TreePath> paths = list != null ?
+ Collections.enumeration(list) : tree.getExpandedDescendants(path);
+ TreePath[] sel = retainSelection ? tree.getSelectionPaths() : null;
+ fireTreeStructureChanged(path);
+ if (paths != null)
+ while (paths.hasMoreElements())
+ tree.expandPath(paths.nextElement());
+ if (sel != null)
+ tree.setSelectionPaths(sel);
+ }
+
+
+
+ protected void fireTreeStructureChanged(TreePath path) {
+ Object[] listeners = listenerList.getListenerList();
+ TreeModelEvent e = null;
+ for (int i = listeners.length-2; i>=0; i-=2) {
+ if (listeners[i]==TreeModelListener.class) {
+ if (e == null)
+ e = new TreeModelEvent(this, path, null, null);
+ ((TreeModelListener)listeners[i+1]).treeStructureChanged(e);
+ }
+ }
+ }
+
+ protected void fireTreeNodesChanged(TreePath path, int[] childIndices, Object[] childNodes) {
+ Object[] listeners = listenerList.getListenerList();
+ TreeModelEvent e = null;
+ for (int i = listeners.length-2; i>=0; i-=2) {
+ if (listeners[i]==TreeModelListener.class) {
+ if (e == null)
+ e = new TreeModelEvent(this, path, childIndices, childNodes);
+ ((TreeModelListener)listeners[i+1]).treeNodesChanged(e);
+ }
+ }
+ }
+
+ protected void fireTreeNodesInserted(TreePath path, int[] childIndices, Object[] childNodes) {
+ Object[] listeners = listenerList.getListenerList();
+ TreeModelEvent e = null;
+ for (int i = listeners.length-2; i>=0; i-=2) {
+ if (listeners[i]==TreeModelListener.class) {
+ if (e == null)
+ e = new TreeModelEvent(this, path, childIndices, childNodes);
+ ((TreeModelListener)listeners[i+1]).treeNodesInserted(e);
+ }
+ }
+ }
+
+ protected void fireTreeNodesRemoved(TreePath path, int[] childIndices, Object[] childNodes) {
+ Object[] listeners = listenerList.getListenerList();
+ TreeModelEvent e = null;
+ for (int i = listeners.length-2; i>=0; i-=2) {
+ if (listeners[i]==TreeModelListener.class) {
+ if (e == null)
+ e = new TreeModelEvent(this, path, childIndices, childNodes);
+ ((TreeModelListener)listeners[i+1]).treeNodesRemoved(e);
+ }
+ }
+ }
+
+
+ protected class Handler implements Comparator<ValueIndexPair<N>>,
+ TreeModelListener, TreeExpansionListener {
+
+ private Comparator<N> comparator;
+
+ private Collator collator = Collator.getInstance();
+
+ void setComparator(Comparator<N> cmp) {
+ comparator = cmp;
+ collator = cmp == null ? Collator.getInstance() : null;
+ }
+
+ Comparator<N> getComparator() {
+ return comparator;
+ }
+
+ // TODO, maybe switch to TreeWillExpandListener?
+ // TreeExpansionListener was used in case an expanded node
+ // had children that would also be expanded, but it is impossible
+ // for hidden nodes' expansion state to survive a SortOrder change
+ // since they are all erased when the tree structure change event
+ // is fired after changing the SortOrder.
+
+ @Override
+ public void treeCollapsed(TreeExpansionEvent e) {}
+
+ @Override
+ public void treeExpanded(TreeExpansionEvent e) {
+ if (sortOrder != SortOrder.UNSORTED) {
+ TreePath path = e.getPath();
+ Converter converter = getConverter(path.getLastPathComponent());
+ if (converter == null) {
+ ArrayList<TreePath> paths = sortHierarchy(path);
+ fireTreeStructureChangedAndExpand(path, paths, false);
+ }
+ }
+ }
+
+ private boolean isFiltered(Object node) {
+ Converter c = getConverter(node);
+ return c == null ? false : c.isFiltered();
+ }
+
+ private boolean acceptable(TreePath path, Object[] childNodes, int index, ArrayList<TreePath> expand) {
+ return acceptable(path, childNodes, index) ||
+ applyFilter(filter, path.pathByAddingChild(childNodes[index]), expand);
+ }
+
+ @Override
+ public void treeNodesChanged(TreeModelEvent e) {
+ treeNodesChanged(e.getTreePath(), e.getChildIndices(), e.getChildren());
+ }
+
+ protected void treeNodesChanged(TreePath path, int[] childIndices, Object[] childNodes) {
+ if (childIndices == null) {
+ // path should be root path
+ // reapply filter
+ if (filter != null)
+ applyFilter(null, null, null, true);
+ return;
+ }
+ Converter converter = getConverter(path.getLastPathComponent());
+ ArrayList<TreePath> expand = null;
+ if (converter != null) {
+ expand = new ArrayList<TreePath>();
+ int childIndex = 0;
+ for (int i=0; i<childIndices.length; i++) {
+ int idx = converter.convertRowIndexToView(childIndices[i]);
+ if (idx >= 0) {
+ // see if the filter dislikes the nodes new state
+ if (converter.isFiltered() &&
+ !isFiltered(childNodes[i]) &&
+ !acceptable(path, childNodes, i)) {
+ // maybe it likes a child nodes state
+ if (!applyFilter(filter, path.pathByAddingChild(childNodes[i]), expand))
+ remove(path, childNodes[i]);
+ continue;
+ }
+ childNodes[childIndex] = childNodes[i];
+ childIndices[childIndex++] = idx;
+ } else if (acceptable(path, childNodes, i, expand)) {
+ int viewIndex = insert(path.getLastPathComponent(),
+ childNodes[i], childIndices[i], converter);
+ fireTreeNodesInserted(path, indices(viewIndex), nodes(childNodes[i]));
+ }
+ }
+ if (childIndex == 0) {
+ maybeFireStructureChange(path, expand);
+ return;
+ }
+ if (sortOrder != SortOrder.UNSORTED && converter.getChildCount() > 1) {
+ sort(path.getLastPathComponent(), createValueIndexPairArray(converter.getChildCount()));
+ fireTreeStructureChangedAndExpand(path, null, true);
+ expandPaths(expand);
+ return;
+ }
+ if (childIndex != childIndices.length) {
+ childIndices = Arrays.copyOf(childIndices, childIndex);
+ childNodes = Arrays.copyOf(childNodes, childIndex);
+ }
+ } else if (filter != null && isFilteredOut(path)) {
+ // see if the filter likes the nodes new states
+ expand = new ArrayList<TreePath>();
+ int[] vtm = null;
+ int idx = 0;
+ for (int i=0; i<childIndices.length; i++) {
+ if (acceptable(path, childNodes, i, expand)) {
+ if (vtm == null)
+ vtm = new int[childIndices.length-i];
+ vtm[idx++] = childIndices[i];
+ }
+ }
+ // filter in path if appropriate
+ if (vtm != null)
+ filterIn(vtm, idx, path, expand);
+ return;
+ }
+ // must fire tree nodes changed even if a
+ // structure change will be fired because the
+ // expanded paths need to be updated first
+ fireTreeNodesChanged(path, childIndices, childNodes);
+ maybeFireStructureChange(path, expand);
+ }
+
+ /**
+ * Helper method for treeNodesChanged...
+ * @param path
+ * @param expand
+ */
+ private void maybeFireStructureChange(TreePath path, ArrayList<TreePath> expand) {
+ if (expand != null && !expand.isEmpty()) {
+ Enumeration<TreePath> expanded = tree.getExpandedDescendants(path);
+ fireTreeStructureChanged(path);
+ if (expanded != null)
+ while (expanded.hasMoreElements())
+ tree.expandPath(expanded.nextElement());
+ expandPaths(expand);
+ }
+ }
+
+ @Override
+ public void treeNodesInserted(TreeModelEvent e) {
+ treeNodesInserted(e.getTreePath(), e.getChildIndices(), e.getChildren());
+ }
+
+ protected void treeNodesInserted(TreePath path, int[] childIndices, Object[] childNodes) {
+ Object parent = path.getLastPathComponent();
+ Converter converter = getConverter(parent);
+ ArrayList<TreePath> expand = null;
+ if (converter != null) {
+// if (childIndices.length > 3 && !converter.isFiltered()
+// && childIndices.length > converter.getChildCount()/10) {
+// TreePath expand = sortHierarchy(path);
+// fireTreeStructureChangedAndExpand(expand);
+// return;
+// }
+ int childIndex = 0;
+ for (int i=0; i<childIndices.length; i++) {
+ if (converter.isFiltered()) {
+ // path hasn't met the filter criteria, so childNodes must be filtered
+ if (expand == null)
+ expand = new ArrayList<TreePath>();
+ if (!applyFilter(filter, path.pathByAddingChild(childNodes[i]), expand))
+ continue;
+ }
+ // shift the appropriate cached modelIndices
+ int[] vtm = converter.viewToModel;
+ int modelIndex = childIndices[i];
+ for (int j=vtm.length; --j>=0;) {
+ if (vtm[j] >= modelIndex)
+ vtm[j] += 1;
+ }
+ // insert modelIndex to converter
+ int viewIndex = insert(parent, childNodes[i], modelIndex, converter);
+ childNodes[childIndex] = childNodes[i];
+ childIndices[childIndex++] = viewIndex;
+ }
+ if (childIndex == 0)
+ return;
+ if (childIndex != childIndices.length) {
+ childIndices = Arrays.copyOf(childIndices, childIndex);
+ childNodes = Arrays.copyOf(childNodes, childIndex);
+ }
+ if (childIndex > 1 && sortOrder != SortOrder.UNSORTED) {
+ sort(childIndices, childNodes);
+ }
+ } else if (filter != null && isFilteredOut(path)) {
+ // apply filter to inserted nodes
+ int[] vtm = null;
+ int idx = 0;
+ expand = new ArrayList<TreePath>();
+ for (int i=0; i<childIndices.length; i++) {
+ if (acceptable(path, childNodes, i, expand)) {
+ if (vtm == null)
+ vtm = new int[childIndices.length-i];
+ vtm[idx++] = childIndices[i];
+ }
+ }
+ // filter in path if appropriate
+ if (vtm != null)
+ filterIn(vtm, idx, path, expand);
+ return;
+ }
+ fireTreeNodesInserted(path, childIndices, childNodes);
+ expandPaths(expand);
+ }
+
+ @Override
+ public void treeNodesRemoved(TreeModelEvent e) {
+ treeNodesRemoved(e.getTreePath(), e.getChildIndices(), e.getChildren());
+ }
+
+
+ private boolean isFilterStartPath(TreePath path) {
+ if (filterStartPath == null)
+ return path.getPathCount() == 1;
+ return filterStartPath.equals(path);
+ }
+
+ protected void treeNodesRemoved(TreePath path, int[] childIndices, Object[] childNodes) {
+ Object parent = path.getLastPathComponent();
+ Converter converter = getConverter(parent);
+ if (converter != null) {
+ int len = 0;
+ for (int i=0; i<childNodes.length; i++) {
+ removeConverter(childNodes[i]);
+ int viewIndex = converter.convertRowIndexToView(childIndices[i]);
+ if (viewIndex >= 0) {
+ childNodes[len] = childNodes[i];
+ childIndices[len++] = viewIndex;
+ }
+ }
+ if (len == 0)
+ return;
+ if (converter.isFiltered() && converter.getChildCount() == len) {
+ ArrayList<TreePath> expand = new ArrayList<TreePath>();
+ if (applyFilter(filter, path, expand)) {
+ expand.retainAll(getExpandedPaths(path));
+ if (sortOrder != SortOrder.UNSORTED)
+ sortHierarchy(path);
+ fireTreeStructureChangedAndExpand(path, expand, true);
+ } else if (isFilterStartPath(path)) {
+ converters.put(parent, new Converter(Converter.EMPTY, true));
+ fireTreeStructureChanged(path);
+ } else {
+ remove(path.getParentPath(), parent);
+ }
+ return;
+ }
+ if (len != childIndices.length) {
+ childIndices = Arrays.copyOf(childIndices, len);
+ childNodes = Arrays.copyOf(childNodes, len);
+ }
+ if (len > 1 && sortOrder != SortOrder.UNSORTED) {
+ sort(childIndices, childNodes);
+ }
+ if (childIndices.length == 1) {
+ converter.remove(converter.convertRowIndexToModel(childIndices[0]));
+ } else {
+ converter.remove(childIndices);
+ }
+ } else if (filter != null && isFilteredOut(path)) {
+ return;
+ }
+ fireTreeNodesRemoved(path, childIndices, childNodes);
+ }
+
+ private Collection<TreePath> getExpandedPaths(TreePath path) {
+ Enumeration<TreePath> en = tree.getExpandedDescendants(path);
+ if (en == null)
+ return Collections.emptySet();
+ HashSet<TreePath> expanded = new HashSet<TreePath>();
+ while (en.hasMoreElements())
+ expanded.add(en.nextElement());
+ return expanded;
+ }
+
+ @Override
+ public void treeStructureChanged(TreeModelEvent e) {
+ if (converters != null) {
+ // not enough information to properly clean up
+ // reapply filter/sort
+ converters = createConvertersMap();
+ TreePath[] sel = tree.getSelectionPaths();
+ if (filter != null) {
+ applyFilter(null, null, getExpandedPaths(new TreePath(getRoot())), false);
+ }
+ if (sortOrder != SortOrder.UNSORTED) {
+ TreePath path = new TreePath(getRoot());
+ ArrayList<TreePath> expand = sortHierarchy(path);
+ fireTreeStructureChangedAndExpand(path, expand, false);
+ }
+ if (sel != null) {
+ tree.clearSelection();
+ TreePath changedPath = e.getTreePath();
+ for (TreePath path : sel) {
+ if (!changedPath.isDescendant(path))
+ tree.addSelectionPath(path);
+ }
+ }
+ } else {
+ fireTreeStructureChanged(e.getTreePath());
+ }
+ }
+
+
+ @Override
+ public final int compare(ValueIndexPair<N> a, ValueIndexPair<N> b) {
+ return compareNodes(a.value, b.value);
+ }
+
+
+ protected int compareNodes(N a, N b) {
+ if (comparator != null)
+ return comparator.compare(a, b);
+ return collator.compare(a.toString(), b.toString());
+ }
+
+ private void removeConverter(Object node) {
+ Converter c = getConverter(node);
+ if (c != null)
+ removeConverter(c, node);
+ }
+
+ private void removeConverter(Converter converter, Object node) {
+ for (int i=converter.getChildCount(); --i>=0;) {
+ int index = converter.convertRowIndexToModel(i);
+ Object child = model.getChild(node, index);
+ Converter c = getConverter(child);
+ if (c != null)
+ removeConverter(c, child);
+ }
+ converters.remove(node);
+ }
+
+ private boolean isFilteredOut(TreePath path) {
+ if (filterStartPath != null && !filterStartPath.isDescendant(path))
+ return false;
+ TreePath parent = path.getParentPath();
+ // root should always have a converter if filter is non-null,
+ // so if parent is ever null, there is a bug somewhere else
+ Converter c = getConverter(parent.getLastPathComponent());
+ if (c != null) {
+ return getIndexOfChild(
+ parent.getLastPathComponent(),
+ path.getLastPathComponent()) < 0;
+ }
+ return isFilteredOut(parent);
+ }
+
+ private void filterIn(int[] vtm, int vtmLength, TreePath path, ArrayList<TreePath> expand) {
+ Object node = path.getLastPathComponent();
+ if (vtmLength != vtm.length)
+ vtm = Arrays.copyOf(vtm, vtmLength);
+ Converter converter = new Converter(vtm, true);
+ converters.put(node, converter);
+ insert(path.getParentPath(), node);
+ tree.expandPath(path);
+ expandPaths(expand);
+ }
+
+ private boolean acceptable(TreePath path, Object[] nodes, int index) {
+ Object node = nodes[index];
+ return filter.acceptNode(path, (N)node, model.isLeaf(node));
+ }
+
+ private int ascInsertionIndex(int[] vtm, Object parent, N node, int idx) {
+ for (int i=vtm.length; --i>=0;) {
+ int cmp = compareNodes(node, (N)model.getChild(parent, vtm[i]));
+ if (cmp > 0 || (cmp == 0 && idx > vtm[i])) {
+ return i+1;
+ }
+ }
+ return 0;
+ }
+
+
+ private int dscInsertionIndex(int[] vtm, Object parent, N node, int idx) {
+ for (int i=vtm.length; --i>=0;) {
+ int cmp = compareNodes(node, (N)model.getChild(parent, vtm[i]));
+ if (cmp < 0) {
+ return i+1;
+ } else if (cmp == 0 && idx < vtm[i]) {
+ return i;
+ }
+ }
+ return 0;
+ }
+
+
+ /**
+ * Inserts the specified path and node and any parent paths as necessary.
+ * <p>
+ * Fires appropriate event.
+ * @param path
+ * @param node
+ */
+ private void insert(TreePath path, Object node) {
+ Object parent = path.getLastPathComponent();
+ Converter converter = converters.get(parent);
+ int modelIndex = model.getIndexOfChild(parent, node);
+ if (converter == null) {
+ converter = new Converter(indices(modelIndex), true);
+ converters.put(parent, converter);
+ insert(path.getParentPath(), parent);
+ } else {
+ int viewIndex = insert(parent, node, modelIndex, converter);
+ fireTreeNodesInserted(path, indices(viewIndex), nodes(node));
+ }
+ }
+
+ /**
+ * Inserts node into parent in correct sort order.
+ * <p>
+ * Responsibility of caller to fire appropriate event with the returned viewIndex.
+ * @param path
+ * @param node
+ * @param modelIndex
+ * @param converter
+ * @return viewIndex
+ */
+ private int insert(Object parent, Object node, int modelIndex, Converter converter) {
+ int[] vtm = converter.viewToModel;
+ int viewIndex;
+ switch (sortOrder) {
+ case ASCENDING:
+ viewIndex = ascInsertionIndex(vtm, parent, (N)node, modelIndex);
+ break;
+ case DESCENDING:
+ viewIndex = dscInsertionIndex(vtm, parent, (N)node, modelIndex);
+ break;
+ default: case UNSORTED:
+ viewIndex = unsortedInsertionIndex(vtm, modelIndex);
+ break;
+ }
+ int[] a = new int[vtm.length+1];
+ System.arraycopy(vtm, 0, a, 0, viewIndex);
+ System.arraycopy(vtm, viewIndex, a, viewIndex+1, vtm.length-viewIndex);
+ a[viewIndex] = modelIndex;
+ converter.viewToModel = a;
+ return viewIndex;
+ }
+
+ private void remove(TreePath path, Object node) {
+ Object parent = path.getLastPathComponent();
+ if (path.getPathCount() == 1 || (filterStartPath != null && filterStartPath.equals(path))) {
+ removeConverter(node);
+ converters.put(parent, new Converter(Converter.EMPTY, true));
+ fireTreeNodesRemoved(path, indices(0), nodes(node));
+ return;
+ }
+ Converter converter = converters.get(parent);
+ int modelIndex = model.getIndexOfChild(parent, node);
+ int viewIndex = converter.remove(modelIndex);
+ switch (viewIndex) {
+ default:
+ removeConverter(node);
+ fireTreeNodesRemoved(path, indices(viewIndex), nodes(node));
+ break;
+ case Converter.ONLY_INDEX:
+// if (path.getParentPath() == null) {
+// // reached filter root
+// removeConverter(node);
+// converters.put(parent, new Converter(Converter.EMPTY, true));
+// fireTreeNodesRemoved(path, indices(0), nodes(node));
+// return;
+// }
+ remove(path.getParentPath(), parent);
+ break;
+ case Converter.INDEX_NOT_FOUND:
+ removeConverter(node);
+ }
+ }
+
+
+
+ }
+
+
+
+ private static int unsortedInsertionIndex(int[] vtm, int idx) {
+ for (int i=vtm.length; --i>=0;)
+ if (vtm[i] < idx)
+ return i+1;
+ return 0;
+ }
+
+ private static void sort(int[] childIndices, Object[] childNodes) {
+ int len = childIndices.length;
+ ValueIndexPair[] pairs = new ValueIndexPair[len];
+ for (int i=len; --i>=0;)
+ pairs[i] = new ValueIndexPair<Object>(childIndices[i], childNodes[i]);
+ Arrays.sort(pairs);
+ for (int i=len; --i>=0;) {
+ childIndices[i] = pairs[i].index;
+ childNodes[i] = pairs[i].value;
+ }
+ }
+
+ private static int[] indices(int...indices) {
+ return indices;
+ }
+
+ private static Object[] nodes(Object...nodes) {
+ return nodes;
+ }
+
+
+
+
+ /**
+ * This class has a dual purpose, both related to comparing/sorting.
+ * <p>
+ * The Handler class sorts an array of ValueIndexPair based on the value.
+ * Used for sorting the view.
+ * <p>
+ * ValueIndexPair sorts itself based on the index.
+ * Used for sorting childIndices for fire* methods.
+ */
+ private static class ValueIndexPair<N> implements Comparable<ValueIndexPair<N>> {
+ ValueIndexPair() {}
+
+ ValueIndexPair(int idx, N val) {
+ index = idx;
+ value = val;
+ }
+
+ N value;
+
+ int index;
+
+ public int compareTo(ValueIndexPair<N> o) {
+ return index - o.index;
+ }
+ }
+
+ private static class Converter {
+
+ static final int[] EMPTY = new int[0];
+
+ static final int ONLY_INDEX = -2;
+
+ static final int INDEX_NOT_FOUND = -1;
+
+ Converter(int[] vtm, boolean filtered) {
+ viewToModel = vtm;
+ isFiltered = filtered;
+ }
+
+ private int[] viewToModel;
+
+ private boolean isFiltered;
+
+// public boolean equals(Converter conv) {
+// if (conv == null)
+// return false;
+// if (isFiltered != conv.isFiltered)
+// return false;
+// return Arrays.equals(viewToModel, conv.viewToModel);
+// }
+
+ boolean isFiltered() {
+ return isFiltered;
+ }
+
+ void remove(int viewIndices[]) {
+ int len = viewToModel.length - viewIndices.length;
+ if (len == 0) {
+ viewToModel = EMPTY;
+ } else {
+ int[] oldVTM = viewToModel;
+ int[] newVTM = new int[len];
+ for (int oldIndex=0, newIndex=0, removeIndex=0;
+ newIndex<newVTM.length;
+ newIndex++, oldIndex++) {
+ while (removeIndex < viewIndices.length && oldIndex == viewIndices[removeIndex]) {
+ int idx = oldVTM[oldIndex];
+ removeIndex++;
+ oldIndex++;
+ for (int i=newIndex; --i>=0;)
+ if (newVTM[i] > idx)
+ newVTM[i]--;
+ for (int i=oldIndex; i<oldVTM.length; i++)
+ if (oldVTM[i] > idx)
+ oldVTM[i]--;
+ }
+ newVTM[newIndex] = oldVTM[oldIndex];
+ }
+ viewToModel = newVTM;
+ }
+ }
+
+ /**
+ * @param modelIndex
+ * @return viewIndex that was removed<br>
+ * or <code>ONLY_INDEX</code> if the modelIndex is the only one in the view<br>
+ * or <code>INDEX_NOT_FOUND</code> if the modelIndex is not in the view
+ */
+ int remove(int modelIndex) {
+ int[] vtm = viewToModel;
+ for (int i=vtm.length; --i>=0;) {
+ if (vtm[i] > modelIndex) {
+ vtm[i] -= 1;
+ } else if (vtm[i] == modelIndex) {
+ if (vtm.length == 1) {
+ viewToModel = EMPTY;
+ return ONLY_INDEX;
+ }
+ int viewIndex = i;
+ while (--i>=0) {
+ if (vtm[i] > modelIndex)
+ vtm[i] -= 1;
+ }
+ int[] a = new int[vtm.length-1];
+ if (viewIndex > 0)
+ System.arraycopy(vtm, 0, a, 0, viewIndex);
+ int len = a.length-viewIndex;
+ if (len > 0)
+ System.arraycopy(vtm, viewIndex+1, a, viewIndex, len);
+ viewToModel = a;
+ return viewIndex;
+ }
+ }
+ return INDEX_NOT_FOUND;
+ }
+
+
+ int getChildCount() {
+ return viewToModel.length;
+ }
+
+ /**
+ * @param modelIndex
+ * @return viewIndex corresponding to modelIndex<br>
+ * or <code>INDEX_NOT_FOUND</code> if the modelIndex is not in the view
+ */
+ int convertRowIndexToView(int modelIndex) {
+ int[] vtm = viewToModel;
+ for (int i=vtm.length; --i>=0;) {
+ if (vtm[i] == modelIndex)
+ return i;
+ }
+ return INDEX_NOT_FOUND;
+ }
+
+ int convertRowIndexToModel(int viewIndex) {
+ return viewToModel[viewIndex];
+ }
+ }
+
+ public interface Filter<N> {
+ boolean acceptNode(TreePath parent, N node, boolean leaf);
+ }
+
+ public static class RegexFilter<N> implements Filter<N> {
+
+ public RegexFilter(Pattern pattern, boolean leaf) {
+ matcher = pattern.matcher("");
+ leafOnly = leaf;
+ }
+
+ private Matcher matcher;
+
+ private boolean leafOnly;
+
+ public boolean acceptNode(TreePath parent, N node, boolean leaf) {
+ if (leafOnly && !leaf)
+ return false;
+ matcher.reset(getStringValue(node));
+ return matcher.find();
+ }
+
+ protected String getStringValue(N node) {
+ return node.toString();
+ }
+ }
+
+
+ private static Map<Object,Converter> createConvertersMap() {
+ return new HashMap<Object,Converter>();
+ }
+}