/* * $Id: JXList.java,v 1.83 2009/05/07 09:21:23 kleopatra Exp $ * * Copyright 2004 Sun Microsystems, Inc., 4150 Network Circle, * Santa Clara, California 95054, U.S.A. All rights reserved. * * 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; either * version 2.1 of the License, or (at your option) any later version. * * 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. * * You should have received a copy of the GNU Lesser General Public * License along with this library; if not, write to the Free Software * Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA */ package org.jdesktop.swingx; import java.awt.Component; import java.awt.Point; import java.awt.event.ActionEvent; import java.util.ArrayList; import java.util.Collections; import java.util.Comparator; import java.util.List; import java.util.Vector; import java.util.logging.Logger; import javax.swing.AbstractListModel; import javax.swing.Action; import javax.swing.JComponent; import javax.swing.JList; import javax.swing.KeyStroke; import javax.swing.ListCellRenderer; import javax.swing.ListModel; import javax.swing.ListSelectionModel; import javax.swing.SwingUtilities; import javax.swing.event.ChangeEvent; import javax.swing.event.ChangeListener; import javax.swing.event.ListDataEvent; import javax.swing.event.ListDataListener; import org.jdesktop.swingx.decorator.ComponentAdapter; import org.jdesktop.swingx.decorator.CompoundHighlighter; import org.jdesktop.swingx.decorator.DefaultSelectionMapper; import org.jdesktop.swingx.decorator.FilterPipeline; import org.jdesktop.swingx.decorator.Highlighter; import org.jdesktop.swingx.decorator.PipelineEvent; import org.jdesktop.swingx.decorator.PipelineListener; import org.jdesktop.swingx.decorator.SelectionMapper; import org.jdesktop.swingx.decorator.SortController; import org.jdesktop.swingx.decorator.SortKey; import org.jdesktop.swingx.decorator.SortOrder; import org.jdesktop.swingx.renderer.AbstractRenderer; import org.jdesktop.swingx.renderer.DefaultListRenderer; import org.jdesktop.swingx.renderer.StringValue; import org.jdesktop.swingx.renderer.StringValues; import org.jdesktop.swingx.rollover.ListRolloverController; import org.jdesktop.swingx.rollover.ListRolloverProducer; import org.jdesktop.swingx.rollover.RolloverProducer; import org.jdesktop.swingx.rollover.RolloverRenderer; import org.jdesktop.swingx.search.ListSearchable; import org.jdesktop.swingx.search.SearchFactory; import org.jdesktop.swingx.search.Searchable; /** * Enhanced List component with support for general SwingX sorting/filtering, * rendering, highlighting, rollover and search functionality. List specific * enhancements include ?? PENDING JW ... * * <h2>Sorting and Filtering</h2> * * JXList supports sorting and filtering. * * It provides api to apply a specific sort order, to toggle the sort order and to reset a sort. * Sort sequence can be configured by setting a custom comparator. * * <pre><code> * list.setFilterEnabled(true); * list.setComparator(myComparator); * list.setSortOrder(SortOrder.DESCENDING); * list.toggleSortOder(); * list.resetSortOrder(); * </code></pre> * * <p> * Rows can be filtered from a JXList using a Filter class and a * FilterPipeline. One assigns a FilterPipeline to the table using * {@link #setFilters(FilterPipeline)}. Filtering hides, but does not delete nor * permanently remove rows from a JXList. * * <p> * JXList provides api to access items of the underlying model in view coordinates * and to convert from/to model coordinates. * * <b>Note</b>: List sorting/filtering is disabled by * default because it has side-effects which might break "normal" expectations * when using a JList: if enabled all row coordinates (including those returned * by the selection) are in view coordinates. Furthermore, the model returned * from getModel() is a wrapper around the actual data. * * <b>Note:</b> SwingX sorting/filtering is incompatible with core sorting/filtering in * JDK 6+. Will be replaced by core functionality after switching the target jdk * version from 5 to 6. * * * <h2>Rendering and Highlighting</h2> * * As all SwingX collection views, a JXList is a HighlighterClient (PENDING JW: * formally define and implement, like in AbstractTestHighlighter), that is it * provides consistent api to add and remove Highlighters which can visually * decorate the rendering component. * <p> * * <pre><code> * * JXList list = new JXList(new Contributors()); * // implement a custom string representation, concated from first-, lastName * StringValue sv = new StringValue() { * public String getString(Object value) { * if (value instanceof Contributor) { * Contributor contributor = (Contributor) value; * return contributor.lastName() + ", " + contributor.firstName(); * } * return StringValues.TO_STRING(value); * } * }; * list.setCellRenderer(new DefaultListRenderer(sv); * // highlight condition: gold merits * HighlightPredicate predicate = new HighlightPredicate() { * public boolean isHighlighted(Component renderer, * ComponentAdapter adapter) { * if (!(value instanceof Contributor)) return false; * return ((Contributor) value).hasGold(); * } * }; * // highlight with foreground color * list.addHighlighter(new PainterHighlighter(predicate, goldStarPainter); * * </code></pre> * * <i>Note:</i> to support the highlighting this implementation wraps the * ListCellRenderer set by client code with a DelegatingRenderer which applies * the Highlighter after delegating the default configuration to the wrappee. As * a side-effect, getCellRenderer does return the wrapper instead of the custom * renderer. To access the latter, client code must call getWrappedCellRenderer. * <p> * * <h2>Rollover</h2> * * As all SwingX collection views, a JXList supports per-cell rollover. If * enabled, the component fires rollover events on enter/exit of a cell which by * default is promoted to the renderer if it implements RolloverRenderer, that * is simulates live behaviour. The rollover events can be used by client code * as well, f.i. to decorate the rollover row using a Highlighter. * * <pre><code> * * JXList list = new JXList(); * list.setRolloverEnabled(true); * list.setCellRenderer(new DefaultListRenderer()); * list.addHighlighter(new ColorHighlighter(HighlightPredicate.ROLLOVER_ROW, * null, Color.RED); * * </code></pre> * * * <h2>Search</h2> * * As all SwingX collection views, a JXList is searchable. A search action is * registered in its ActionMap under the key "find". The default behaviour is to * ask the SearchFactory to open a search component on this component. The * default keybinding is retrieved from the SearchFactory, typically ctrl-f (or * cmd-f for Mac). Client code can register custom actions and/or bindings as * appropriate. * <p> * * JXList provides api to vend a renderer-controlled String representation of * cell content. This allows the Searchable and Highlighters to use WYSIWYM * (What-You-See-Is-What-You-Match), that is pattern matching against the actual * string as seen by the user. * * * @author Ramesh Gupta * @author Jeanette Winzenburg */ public class JXList extends JList { @SuppressWarnings("all") private static final Logger LOG = Logger.getLogger(JXList.class.getName()); public static final String EXECUTE_BUTTON_ACTIONCOMMAND = "executeButtonAction"; /** The pipeline holding the filters. */ protected FilterPipeline filters; /** * The pipeline holding the highlighters. */ protected CompoundHighlighter compoundHighlighter; /** listening to changeEvents from compoundHighlighter. */ private ChangeListener highlighterChangeListener; /** The ComponentAdapter for model data access. */ protected ComponentAdapter dataAdapter; /** * Mouse/Motion/Listener keeping track of mouse moved in cell coordinates. */ private RolloverProducer rolloverProducer; /** * RolloverController: listens to cell over events and repaints * entered/exited rows. */ private ListRolloverController<JXList> linkController; /** A wrapper around the default renderer enabling decoration. */ private DelegatingRenderer delegatingRenderer; private WrappingListModel wrappingModel; private PipelineListener pipelineListener; private boolean filterEnabled; private SelectionMapper selectionMapper; private Searchable searchable; private Comparator<?> comparator; /** * Constructs a <code>JXList</code> with an empty model and filters disabled. * */ public JXList() { this(false); } /** * Constructs a <code>JXList</code> that displays the elements in the * specified, non-<code>null</code> model and filters disabled. * * @param dataModel the data model for this list * @exception IllegalArgumentException if <code>dataModel</code> * is <code>null</code> */ public JXList(ListModel dataModel) { this(dataModel, false); } /** * Constructs a <code>JXList</code> that displays the elements in * the specified array and filters disabled. * * @param listData the array of Objects to be loaded into the data model * @throws IllegalArgumentException if <code>listData</code> * is <code>null</code> */ public JXList(Object[] listData) { this(listData, false); } /** * Constructs a <code>JXList</code> that displays the elements in * the specified <code>Vector</code> and filtes disabled. * * @param listData the <code>Vector</code> to be loaded into the * data model * @throws IllegalArgumentException if <code>listData</code> * is <code>null</code> */ public JXList(Vector<?> listData) { this(listData, false); } /** * Constructs a <code>JXList</code> with an empty model and * filterEnabled property. * * @param filterEnabled <code>boolean</code> to determine if * filtering/sorting is enabled */ public JXList(boolean filterEnabled) { init(filterEnabled); } /** * Constructs a <code>JXList</code> with the specified model and * filterEnabled property. * * @param dataModel the data model for this list * @param filterEnabled <code>boolean</code> to determine if * filtering/sorting is enabled * @throws IllegalArgumentException if <code>dataModel</code> * is <code>null</code> */ public JXList(ListModel dataModel, boolean filterEnabled) { super(dataModel); init(filterEnabled); } /** * Constructs a <code>JXList</code> that displays the elements in * the specified array and filterEnabled property. * * @param listData the array of Objects to be loaded into the data model * @param filterEnabled <code>boolean</code> to determine if filtering/sorting * is enabled * @throws IllegalArgumentException if <code>listData</code> * is <code>null</code> */ public JXList(Object[] listData, boolean filterEnabled) { super(listData); if (listData == null) throw new IllegalArgumentException("listData must not be null"); init(filterEnabled); } /** * Constructs a <code>JXList</code> that displays the elements in * the specified <code>Vector</code> and filtersEnabled property. * * @param listData the <code>Vector</code> to be loaded into the * data model * @param filterEnabled <code>boolean</code> to determine if filtering/sorting * is enabled * @throws IllegalArgumentException if <code>listData</code> is <code>null</code> */ public JXList(Vector<?> listData, boolean filterEnabled) { super(listData); if (listData == null) throw new IllegalArgumentException("listData must not be null"); init(filterEnabled); } private void init(boolean filterEnabled) { setFilterEnabled(filterEnabled); Action findAction = createFindAction(); getActionMap().put("find", findAction); KeyStroke findStroke = SearchFactory.getInstance().getSearchAccelerator(); getInputMap(JComponent.WHEN_ANCESTOR_OF_FOCUSED_COMPONENT).put(findStroke, "find"); } private Action createFindAction() { return new UIAction("find") { public void actionPerformed(ActionEvent e) { doFind(); } }; } /** * Starts a search on this List's visible items. This implementation asks the * SearchFactory to open a find widget on itself. */ protected void doFind() { SearchFactory.getInstance().showFindInput(this, getSearchable()); } /** * Returns a Searchable for this component, guaranteed to be not null. This * implementation lazily creates a ListSearchable if necessary. * * @return a not-null Searchable for this list. * * @see #setSearchable(Searchable) * @see org.jdesktop.swingx.search.ListSearchable */ public Searchable getSearchable() { if (searchable == null) { searchable = new ListSearchable(this); } return searchable; } /** * Sets the Searchable for this component. If null, a default * Searchable will be created and used. * * @param searchable the Searchable to use for this component, may be null to indicate * using the list's default searchable. * @see #getSearchable() */ public void setSearchable(Searchable searchable) { this.searchable = searchable; } //--------------------- Rollover support /** * Sets the property to enable/disable rollover support. If enabled, the list * fires property changes on per-cell mouse rollover state, i.e. * when the mouse enters/leaves a list cell. <p> * * This can be enabled to show "live" rollover behaviour, f.i. the cursor over a cell * rendered by a JXHyperlink.<p> * * Default value is disabled. * * @param rolloverEnabled a boolean indicating whether or not the rollover * functionality should be enabled. * * @see #isRolloverEnabled() * @see #getLinkController() * @see #createRolloverProducer() * @see org.jdesktop.swingx.rollover.RolloverRenderer * */ public void setRolloverEnabled(boolean rolloverEnabled) { boolean old = isRolloverEnabled(); if (rolloverEnabled == old) return; if (rolloverEnabled) { rolloverProducer = createRolloverProducer(); addMouseListener(rolloverProducer); addMouseMotionListener(rolloverProducer); getLinkController().install(this); } else { removeMouseListener(rolloverProducer); removeMouseMotionListener(rolloverProducer); rolloverProducer = null; getLinkController().release(); } firePropertyChange("rolloverEnabled", old, isRolloverEnabled()); } /** * Returns a boolean indicating whether or not rollover support is enabled. * * @return a boolean indicating whether or not rollover support is enabled. * * @see #setRolloverEnabled(boolean) */ public boolean isRolloverEnabled() { return rolloverProducer != null; } /** * Returns the RolloverController for this component. Lazyly creates the * controller if necessary, that is the return value is guaranteed to be * not null. <p> * * PENDING JW: rename to getRolloverController * * @return the RolloverController for this tree, guaranteed to be not null. * * @see #setRolloverEnabled(boolean) * @see #createLinkController() * @see org.jdesktop.swingx.rollover.RolloverController */ protected ListRolloverController<JXList> getLinkController() { if (linkController == null) { linkController = createLinkController(); } return linkController; } /** * Creates and returns a RolloverController appropriate for this component. * * @return a RolloverController appropriate for this component. * * @see #getLinkController() * @see org.jdesktop.swingx.rollover.RolloverController */ protected ListRolloverController<JXList> createLinkController() { return new ListRolloverController<JXList>(); } /** * Creates and returns the RolloverProducer to use with this tree. * <p> * * @return <code>RolloverProducer</code> to use with this tree * * @see #setRolloverEnabled(boolean) */ protected RolloverProducer createRolloverProducer() { return new ListRolloverProducer(); } //--------------------- public sort api // /** // * Returns the sortable property. // * Here: same as filterEnabled. // * @return true if the table is sortable. // */ // public boolean isSortable() { // return isFilterEnabled(); // } /** * Removes the interactive sorter. * */ public void resetSortOrder() { SortController controller = getSortController(); if (controller != null) { controller.setSortKeys(null); } } /** * * Toggles the sort order of the items. * <p> * The exact behaviour is defined by the SortController's * toggleSortOrder implementation. Typically a unsorted * column is sorted in ascending order, a sorted column's * order is reversed. * <p> * PENDING: where to get the comparator from? * <p> * * */ public void toggleSortOrder() { SortController controller = getSortController(); if (controller != null) { controller.toggleSortOrder(0, getComparator()); } } /** * Sorts the list using SortOrder. * * * Respects the JXList's sortable and comparator * properties: routes the comparator to the SortController * and does nothing if !isFilterEnabled(). * <p> * * @param sortOrder the sort order to use. If null or SortOrder.UNSORTED, * this method has the same effect as resetSortOrder(); * */ public void setSortOrder(SortOrder sortOrder) { if ((sortOrder == null) || !sortOrder.isSorted()) { resetSortOrder(); return; } SortController sortController = getSortController(); if (sortController != null) { SortKey sortKey = new SortKey(sortOrder, 0, getComparator()); sortController.setSortKeys(Collections.singletonList(sortKey)); } } /** * Returns the SortOrder. * * @return the interactive sorter's SortOrder * or SortOrder.UNSORTED */ public SortOrder getSortOrder() { SortController sortController = getSortController(); if (sortController == null) return SortOrder.UNSORTED; SortKey sortKey = SortKey.getFirstSortKeyForColumn(sortController.getSortKeys(), 0); return sortKey != null ? sortKey.getSortOrder() : SortOrder.UNSORTED; } /** * * @return the comparator used. * @see #setComparator(Comparator) */ public Comparator<?> getComparator() { return comparator; } /** * Sets the comparator used. As a side-effect, the * current sort might be updated. The exact behaviour * is defined in #updateSortAfterComparatorChange. * * @param comparator the comparator to use. */ public void setComparator(Comparator<?> comparator) { Comparator<?> old = getComparator(); this.comparator = comparator; updateSortAfterComparatorChange(); firePropertyChange("comparator", old, getComparator()); } /** * Updates sort after comparator has changed. * Here: sets the current sortOrder with the new comparator. * */ protected void updateSortAfterComparatorChange() { setSortOrder(getSortOrder()); } /** * returns the currently active SortController. Will be null if * !isFilterEnabled(). * @return the currently active <code>SortController</code> may be null */ protected SortController getSortController() { // // this check is for the sake of the very first call after instantiation // doesn't apply for JXList? need to test for filterEnabled? //if (filters == null) return null; if (!isFilterEnabled()) return null; return getFilters().getSortController(); } // ---------------------------- filters /** * returns the element at the given index. The index is in view coordinates * which might differ from model coordinates if filtering is enabled and * filters/sorters are active. * * @param viewIndex the index in view coordinates * @return the element at the index * @throws IndexOutOfBoundsException if viewIndex < 0 or viewIndex >= * getElementCount() */ public Object getElementAt(int viewIndex) { return getModel().getElementAt(viewIndex); } /** * Returns the number of elements in this list in view * coordinates. If filters are active this number might be * less than the number of elements in the underlying model. * * @return number of elements in this list in view coordinates */ public int getElementCount() { return getModel().getSize(); } /** * Convert row index from view coordinates to model coordinates accounting * for the presence of sorters and filters. * * @param viewIndex index in view coordinates * @return index in model coordinates * @throws IndexOutOfBoundsException if viewIndex < 0 or viewIndex >= getElementCount() */ public int convertIndexToModel(int viewIndex) { return isFilterEnabled() ? getFilters().convertRowIndexToModel( viewIndex) : viewIndex; } /** * Convert index from model coordinates to view coordinates accounting * for the presence of sorters and filters. * * PENDING Filter guards against out of range - should not? * * @param modelIndex index in model coordinates * @return index in view coordinates if the model index maps to a view coordinate * or -1 if not contained in the view. * */ public int convertIndexToView(int modelIndex) { return isFilterEnabled() ? getFilters().convertRowIndexToView( modelIndex) : modelIndex; } /** * returns the underlying model. If !isFilterEnabled this will be the same * as getModel(). * * @return the underlying model */ public ListModel getWrappedModel() { return isFilterEnabled() ? wrappingModel.getModel() : getModel(); } /** * Enables/disables filtering support. If enabled all row indices - * including the selection - are in view coordinates and getModel returns a * wrapper around the underlying model. * * Note: as an implementation side-effect calling this method clears the * selection (done in super.setModel). * * PENDING: cleanup state transitions!! - currently this can be safely * applied once only to enable. Internal state is inconsistent if trying to * disable again. As a temporary emergency measure, this will throw a * IllegalStateException. * * see Issue #2-swinglabs. * * @param enabled * @throws IllegalStateException if trying to disable again. */ public void setFilterEnabled(boolean enabled) { boolean old = isFilterEnabled(); if (old == enabled) return; if (old) throw new IllegalStateException("must not reset filterEnabled"); // JW: filterEnabled must be set before calling super.setModel! filterEnabled = enabled; wrappingModel = new WrappingListModel(getModel()); super.setModel(wrappingModel); firePropertyChange("filterEnabled", old, isFilterEnabled()); } /** * * @return a <boolean> indicating if filtering is enabled. * @see #setFilterEnabled(boolean) */ public boolean isFilterEnabled() { return filterEnabled; } /** * {@inheritDoc} <p> * * Overridden to update selectionMapper */ @Override public void setSelectionModel(ListSelectionModel newModel) { super.setSelectionModel(newModel); getSelectionMapper().setViewSelectionModel(getSelectionModel()); } /** * {@inheritDoc} <p> * * Sets the underlying data model. Note that if isFilterEnabled you must * call getWrappedModel to access the model given here. In this case * getModel returns a wrapper around the data! * * @param model the data model for this list. * */ @Override public void setModel(ListModel model) { if (isFilterEnabled()) { wrappingModel.setModel(model); } else { super.setModel(model); } } /** * widened access for testing... * @return the selection mapper */ protected SelectionMapper getSelectionMapper() { if (selectionMapper == null) { selectionMapper = new DefaultSelectionMapper(filters, getSelectionModel()); } return selectionMapper; } /** * Returns the FilterPipeline assigned to this list, or null if filtering not * enabled. * * @return the <code>FilterPipeline</code> assigned to this list, or * null if !isFiltersEnabled(). */ public FilterPipeline getFilters() { if ((filters == null) && isFilterEnabled()) { setFilters(null); } return filters; } /** Sets the FilterPipeline for filtering the items of this list, maybe null * to remove all previously applied filters. * * Note: the current "interactive" sortState is preserved (by * internally copying the old sortKeys to the new pipeline, if any). * * PRE: isFilterEnabled() * * @param pipeline the <code>FilterPipeline</code> to use, null removes * all filters. * @throws IllegalStateException if !isFilterEnabled() */ public void setFilters(FilterPipeline pipeline) { if (!isFilterEnabled()) throw new IllegalStateException("filters not enabled - not allowed to set filters"); FilterPipeline old = filters; List<? extends SortKey> sortKeys = null; if (old != null) { old.removePipelineListener(pipelineListener); sortKeys = old.getSortController().getSortKeys(); } if (pipeline == null) { pipeline = new FilterPipeline(); } filters = pipeline; filters.getSortController().setSortKeys(sortKeys); // JW: first assign to prevent (short?) illegal internal state // #173-swingx use(filters); getSelectionMapper().setFilters(filters); } /** * setModel() and setFilters() may be called in either order. * * @param pipeline */ private void use(FilterPipeline pipeline) { if (pipeline != null) { // check JW: adding listener multiple times (after setModel)? if (initialUse(pipeline)) { pipeline.addPipelineListener(getFilterPipelineListener()); pipeline.assign(getComponentAdapter()); } else { pipeline.flush(); } } } /** * @return true is not yet used in this JXTable, false otherwise */ private boolean initialUse(FilterPipeline pipeline) { if (pipelineListener == null) return true; PipelineListener[] l = pipeline.getPipelineListeners(); for (int i = 0; i < l.length; i++) { if (pipelineListener.equals(l[i])) return false; } return true; } /** returns the listener for changes in filters. */ protected PipelineListener getFilterPipelineListener() { if (pipelineListener == null) { pipelineListener = createPipelineListener(); } return pipelineListener; } /** creates the listener for changes in filters. */ protected PipelineListener createPipelineListener() { return new PipelineListener() { public void contentsChanged(PipelineEvent e) { updateOnFilterContentChanged(); } }; } /** * method called on change notification from filterpipeline. */ protected void updateOnFilterContentChanged() { // make the wrapper listen to the pipeline? if (wrappingModel != null) { wrappingModel.updateOnFilterContentChanged(); } revalidate(); repaint(); } private class WrappingListModel extends AbstractListModel { private ListModel delegate; private ListDataListener listDataListener; private Point OUTSIDE = new Point(-1, -1); protected boolean ignoreFilterContentChanged; public WrappingListModel(ListModel model) { setModel(model); } public void updateOnFilterContentChanged() { if (ignoreFilterContentChanged) return; fireContentsChanged(this, -1, -1); } public void setModel(ListModel model) { ListModel old = this.getModel(); if (old != null) { old.removeListDataListener(listDataListener); } this.delegate = model; delegate.addListDataListener(getListDataListener()); // sequence of method calls? // fire contentsChanged after internal cleanup? fireContentsChanged(this, -1, -1); // fix #477-swingx getSelectionMapper().clearModelSelection(); getFilters().flush(); } private ListDataListener getListDataListener() { if (listDataListener == null) { listDataListener = createListDataListener(); } return listDataListener; } private ListDataListener createListDataListener() { return new ListDataListener() { public void intervalAdded(ListDataEvent e) { boolean wasEnabled = getSelectionMapper().isEnabled(); getSelectionMapper().setEnabled(false); try { updateModelSelection(e); ignoreFilterContentChanged = true; getFilters().flush(); ignoreFilterContentChanged = false; // do the mapping after the flush and refire refireMappedEvent(getMappedEvent(e)); } finally { // for mutations, super and UI must be done with updating their internals // before it's safe to synch the view selection getSelectionMapper().setEnabled(wasEnabled); } } public void intervalRemoved(ListDataEvent e) { boolean wasEnabled = getSelectionMapper().isEnabled(); getSelectionMapper().setEnabled(false); try { updateModelSelection(e); // do the mapping before flushing // otherwise we may get indexOOBs ListDataEvent mappedEvent = getMappedEvent(e); ignoreFilterContentChanged = true; getFilters().flush(); ignoreFilterContentChanged = false; refireMappedEvent(mappedEvent); } finally { // for mutations, super and UI must be done with updating their internals // before it's safe to synch the view selection getSelectionMapper().setEnabled(wasEnabled); } } public void contentsChanged(ListDataEvent e) { updateInternals(e); refireContentsChanged(e); } }; } /** * Refires the received event. Tries its best to map to the new * coordinates. At this point, the internals (selection, filter) are * updated, so it's safe to use the conversion methods. * * @param e the ListDataEvent received from the wrapped model. */ private void refireContentsChanged(ListDataEvent e) { // quick check for single item removal if ((e.getIndex0() >= 0) && (e.getIndex0() == e.getIndex1())) { // single outside - no notification int viewIndex = convertIndexToView(e.getIndex0()); if (viewIndex == -1) return; fireContentsChanged(this, viewIndex, viewIndex); } else if (e.getIndex0() >= 0) { // PENDING JW: narrow the interval bounds fireContentsChanged(this, 0, getSize()); } else { fireContentsChanged(this, -1, -1); } } /** * @param mappedEvent */ protected void refireMappedEvent(ListDataEvent mappedEvent) { if (mappedEvent == null) return; if (mappedEvent.getType() == ListDataEvent.INTERVAL_REMOVED) { fireIntervalRemoved(this, mappedEvent.getIndex0(), mappedEvent.getIndex1()); } else if (mappedEvent.getType() == ListDataEvent.INTERVAL_ADDED) { fireIntervalAdded(this, mappedEvent.getIndex0(), mappedEvent.getIndex1()); } else { fireContentsChanged(this, mappedEvent.getIndex0(), mappedEvent.getIndex1()); } } private ListDataEvent getMappedEvent(ListDataEvent e) { // quick check for single item removal if ((e.getIndex0() != - 1) && (e.getIndex0() == e.getIndex1())) { int viewIndex = convertIndexToView(e.getIndex0()); // single outside - no notification if (viewIndex == -1) return null; return new ListDataEvent(this, e.getType(), viewIndex, viewIndex); } Point mappedRange = getContinousMappedRange(e); if (mappedRange == null) { // cant help - no support for discontiouns interval remove notification return new ListDataEvent(this, ListDataEvent.CONTENTS_CHANGED, -1, -1); } else if (OUTSIDE == mappedRange) { return null; // do nothing, everything is outside } // could map to a continous interval return new ListDataEvent(this, e.getType(), mappedRange.x, mappedRange.y); } protected Point getContinousMappedRange(ListDataEvent e) { List<Integer> mapped = new ArrayList<Integer>(); for (int i = e.getIndex0(); i <= e.getIndex1(); i++) { int viewIndex = convertIndexToView(i); if (viewIndex >= 0) { mapped.add(viewIndex); } } if (mapped.size() == 0) return OUTSIDE; if (mapped.size() == 1) return new Point(mapped.get(0), mapped.get(0)); Collections.sort(mapped); for (int i = 0; i < mapped.size() - 2; i++) { if (mapped.get(i+1) - mapped.get(i) != 1) return null; } return new Point(mapped.get(0), mapped.get(mapped.size() - 1)); } private void updateInternals(ListDataEvent e) { boolean wasEnabled = getSelectionMapper().isEnabled(); getSelectionMapper().setEnabled(false); try { updateModelSelection(e); } finally { getSelectionMapper().setEnabled(wasEnabled); } ignoreFilterContentChanged = true; getFilters().flush(); ignoreFilterContentChanged = false; } /** * Adjusts the model coordinates of the selection as appropriate * for the given event. * * @param e the ListDataEvent to adjust from. */ protected void updateModelSelection(ListDataEvent e) { if (e.getType() == ListDataEvent.INTERVAL_REMOVED) { getSelectionMapper() .removeIndexInterval(e.getIndex0(), e.getIndex1()); } else if (e.getType() == ListDataEvent.INTERVAL_ADDED) { int minIndex = Math.min(e.getIndex0(), e.getIndex1()); int maxIndex = Math.max(e.getIndex0(), e.getIndex1()); int length = maxIndex - minIndex + 1; getSelectionMapper().insertIndexInterval(minIndex, length, true); } else if (e.getIndex0() == -1) { getSelectionMapper().clearModelSelection(); } } public ListModel getModel() { return delegate; } public int getSize() { return getFilters().getOutputSize(); } public Object getElementAt(int index) { return getFilters().getValueAt(index, 0); } } // ---------------------------- uniform data model /** * @return the unconfigured ComponentAdapter. */ protected ComponentAdapter getComponentAdapter() { if (dataAdapter == null) { dataAdapter = new ListAdapter(this); } return dataAdapter; } /** * Convenience to access a configured ComponentAdapter. * Note: the column index of the configured adapter is always 0. * * @param index the row index in view coordinates, must be valid. * @return the configured ComponentAdapter. */ protected ComponentAdapter getComponentAdapter(int index) { ComponentAdapter adapter = getComponentAdapter(); adapter.column = 0; adapter.row = index; return adapter; } /** * A component adapter targeted at a JXList. */ protected static class ListAdapter extends ComponentAdapter { private final JXList list; /** * Constructs a <code>ListAdapter</code> for the specified target * JXList. * * @param component the target list. */ public ListAdapter(JXList component) { super(component); list = component; } /** * Typesafe accessor for the target component. * * @return the target component as a {@link org.jdesktop.swingx.JXList} */ public JXList getList() { return list; } /** * {@inheritDoc} */ @Override public boolean hasFocus() { /** TODO: Think through printing implications */ return list.isFocusOwner() && (row == list.getLeadSelectionIndex()); } /** * {@inheritDoc} */ @Override public int getRowCount() { return list.getWrappedModel().getSize(); } /** * {@inheritDoc} <p> * Overridden to return value at implicit view coordinates. */ @Override public Object getValue() { return list.getElementAt(row); } /** * {@inheritDoc} */ @Override public Object getValueAt(int row, int column) { return list.getWrappedModel().getElementAt(row); } /** * {@inheritDoc} */ @Override public Object getFilteredValueAt(int row, int column) { return list.getElementAt(row); } /** * {@inheritDoc} */ @Override public String getFilteredStringAt(int row, int column) { return list.getStringAt(row); } /** * {@inheritDoc} */ @Override public String getString() { return list.getStringAt(row); } /** * {@inheritDoc} */ @Override public String getStringAt(int row, int column) { // PENDING JW: here we are duplicating code from the list // that's because list api is in view-coordinates ListCellRenderer renderer = list.getDelegatingRenderer().getDelegateRenderer(); if (renderer instanceof StringValue) { return ((StringValue) renderer).getString(getValueAt(row, column)); } return StringValues.TO_STRING.getString(getValueAt(row, column)); } /** * {@inheritDoc} */ @Override public void setValueAt(Object aValue, int row, int column) { throw new UnsupportedOperationException( "The model is immutable."); } /** * {@inheritDoc} */ @Override public boolean isCellEditable(int row, int column) { return false; } /** * {@inheritDoc} */ @Override public boolean isEditable() { return false; } /** * {@inheritDoc} */ @Override public boolean isSelected() { /** TODO: Think through printing implications */ return list.isSelectedIndex(row); } } // ------------------------------ renderers /** * Sets the <code>Highlighter</code>s to the table, replacing any old settings. * None of the given Highlighters must be null.<p> * * This is a bound property. <p> * * Note: as of version #1.257 the null constraint is enforced strictly. To remove * all highlighters use this method without param. * * @param highlighters zero or more not null highlighters to use for renderer decoration. * @throws NullPointerException if array is null or array contains null values. * * @see #getHighlighters() * @see #addHighlighter(Highlighter) * @see #removeHighlighter(Highlighter) * */ public void setHighlighters(Highlighter... highlighters) { Highlighter[] old = getHighlighters(); getCompoundHighlighter().setHighlighters(highlighters); firePropertyChange("highlighters", old, getHighlighters()); } /** * Returns the <code>Highlighter</code>s used by this table. * Maybe empty, but guarantees to be never null. * * @return the Highlighters used by this table, guaranteed to never null. * @see #setHighlighters(Highlighter[]) */ public Highlighter[] getHighlighters() { return getCompoundHighlighter().getHighlighters(); } /** * Appends a <code>Highlighter</code> to the end of the list of used * <code>Highlighter</code>s. The argument must not be null. * <p> * * @param highlighter the <code>Highlighter</code> to add, must not be null. * @throws NullPointerException if <code>Highlighter</code> is null. * * @see #removeHighlighter(Highlighter) * @see #setHighlighters(Highlighter[]) */ public void addHighlighter(Highlighter highlighter) { Highlighter[] old = getHighlighters(); getCompoundHighlighter().addHighlighter(highlighter); firePropertyChange("highlighters", old, getHighlighters()); } /** * Removes the given Highlighter. <p> * * Does nothing if the Highlighter is not contained. * * @param highlighter the Highlighter to remove. * @see #addHighlighter(Highlighter) * @see #setHighlighters(Highlighter...) */ public void removeHighlighter(Highlighter highlighter) { Highlighter[] old = getHighlighters(); getCompoundHighlighter().removeHighlighter(highlighter); firePropertyChange("highlighters", old, getHighlighters()); } /** * Returns the CompoundHighlighter assigned to the table, null if none. * PENDING: open up for subclasses again?. * * @return the CompoundHighlighter assigned to the table. */ protected CompoundHighlighter getCompoundHighlighter() { if (compoundHighlighter == null) { compoundHighlighter = new CompoundHighlighter(); compoundHighlighter.addChangeListener(getHighlighterChangeListener()); } return compoundHighlighter; } /** * Returns the <code>ChangeListener</code> to use with highlighters. Lazily * creates the listener. * * @return the ChangeListener for observing changes of highlighters, * guaranteed to be <code>not-null</code> */ protected ChangeListener getHighlighterChangeListener() { if (highlighterChangeListener == null) { highlighterChangeListener = createHighlighterChangeListener(); } return highlighterChangeListener; } /** * Creates and returns the ChangeListener observing Highlighters. * <p> * Here: repaints the table on receiving a stateChanged. * * @return the ChangeListener defining the reaction to changes of * highlighters. */ protected ChangeListener createHighlighterChangeListener() { return new ChangeListener() { public void stateChanged(ChangeEvent e) { repaint(); } }; } /** * Returns the string representation of the cell value at the given position. * * @param row the row index of the cell in view coordinates * @return the string representation of the cell value as it will appear in the * table. */ public String getStringAt(int row) { ListCellRenderer renderer = getDelegatingRenderer().getDelegateRenderer(); if (renderer instanceof StringValue) { return ((StringValue) renderer).getString(getElementAt(row)); } return StringValues.TO_STRING.getString(getElementAt(row)); } private DelegatingRenderer getDelegatingRenderer() { if (delegatingRenderer == null) { // only called once... to get hold of the default? delegatingRenderer = new DelegatingRenderer(); } return delegatingRenderer; } /** * Creates and returns the default cell renderer to use. Subclasses * may override to use a different type. Here: returns a <code>DefaultListRenderer</code>. * * @return the default cell renderer to use with this list. */ protected ListCellRenderer createDefaultCellRenderer() { return new DefaultListRenderer(); } /** * {@inheritDoc} <p> * * Overridden to return the delegating renderer which is wrapped around the * original to support highlighting. The returned renderer is of type * DelegatingRenderer and guaranteed to not-null<p> * * @see #setCellRenderer(ListCellRenderer) * @see DelegatingRenderer */ @Override public ListCellRenderer getCellRenderer() { return getDelegatingRenderer(); } /** * Returns the renderer installed by client code or the default if none has * been set. * * @return the wrapped renderer. * @see #setCellRenderer(ListCellRenderer) */ public ListCellRenderer getWrappedCellRenderer() { return getDelegatingRenderer().getDelegateRenderer(); } /** * {@inheritDoc} <p> * * Overridden to wrap the given renderer in a DelegatingRenderer to support * highlighting. <p> * * Note: the wrapping implies that the renderer returned from the getCellRenderer * is <b>not</b> the renderer as given here, but the wrapper. To access the original, * use <code>getWrappedCellRenderer</code>. * * @see #getWrappedCellRenderer() * @see #getCellRenderer() * */ @Override public void setCellRenderer(ListCellRenderer renderer) { // JW: Pending - probably fires propertyChangeEvent with wrong newValue? // how about fixedCellWidths? // need to test!! getDelegatingRenderer().setDelegateRenderer(renderer); super.setCellRenderer(delegatingRenderer); } /** * A decorator for the original ListCellRenderer. Needed to hook highlighters * after messaging the delegate.<p> * * PENDING JW: formally implement UIDependent? */ public class DelegatingRenderer implements ListCellRenderer, RolloverRenderer { /** the delegate. */ private ListCellRenderer delegateRenderer; /** * Instantiates a DelegatingRenderer with list's default renderer as delegate. */ public DelegatingRenderer() { this(null); } /** * Instantiates a DelegatingRenderer with the given delegate. If the * delegate is null, the default is created via the list's factory method. * * @param delegate the delegate to use, if null the list's default is * created and used. */ public DelegatingRenderer(ListCellRenderer delegate) { setDelegateRenderer(delegate); } /** * Sets the delegate. If the * delegate is null, the default is created via the list's factory method. * * @param delegate the delegate to use, if null the list's default is * created and used. */ public void setDelegateRenderer(ListCellRenderer delegate) { if (delegate == null) { delegate = createDefaultCellRenderer(); } delegateRenderer = delegate; } /** * Returns the delegate. * * @return the delegate renderer used by this renderer, guaranteed to * not-null. */ public ListCellRenderer getDelegateRenderer() { return delegateRenderer; } /** * Updates the ui of the delegate. */ public void updateUI() { updateRendererUI(delegateRenderer); } /** * * @param renderer the renderer to update the ui of. */ private void updateRendererUI(ListCellRenderer renderer) { if (renderer == null) return; Component comp = null; if (renderer instanceof AbstractRenderer) { comp = ((AbstractRenderer) renderer).getComponentProvider().getRendererComponent(null); } else if (renderer instanceof Component) { comp = (Component) renderer; } else { try { comp = renderer.getListCellRendererComponent( JXList.this, null, -1, false, false); } catch (Exception e) { // nothing to do - renderer barked on off-range row } } if (comp != null) { SwingUtilities.updateComponentTreeUI(comp); } } // --------- implement ListCellRenderer /** * {@inheritDoc} <p> * * Overridden to apply the highlighters, if any, after calling the delegate. * The decorators are not applied if the row is invalid. */ public Component getListCellRendererComponent(JList list, Object value, int index, boolean isSelected, boolean cellHasFocus) { Component comp = delegateRenderer.getListCellRendererComponent(list, value, index, isSelected, cellHasFocus); if ((compoundHighlighter != null) && (index >= 0) && (index < getElementCount())) { comp = compoundHighlighter.highlight(comp, getComponentAdapter(index)); } return comp; } // implement RolloverRenderer /** * {@inheritDoc} * */ public boolean isEnabled() { return (delegateRenderer instanceof RolloverRenderer) && ((RolloverRenderer) delegateRenderer).isEnabled(); } /** * {@inheritDoc} */ public void doClick() { if (isEnabled()) { ((RolloverRenderer) delegateRenderer).doClick(); } } } // --------------------------- updateUI /** * {@inheritDoc} <p> * * Overridden to update renderer and Highlighters. */ @Override public void updateUI() { super.updateUI(); updateRendererUI(); updateHighlighterUI(); } private void updateRendererUI() { if (delegatingRenderer != null) { delegatingRenderer.updateUI(); } else { ListCellRenderer renderer = getCellRenderer(); if (renderer instanceof Component) { SwingUtilities.updateComponentTreeUI((Component) renderer); } } } /** * Updates highlighter after <code>updateUI</code> changes. * * @see org.jdesktop.swingx.decorator.UIDependent */ protected void updateHighlighterUI() { if (compoundHighlighter == null) return; compoundHighlighter.updateUI(); } }