/* * $Id$ * * Copyright 2009 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.hdesktop.swingx.sort; import java.util.ArrayList; import java.util.Arrays; import java.util.Comparator; import java.util.List; import javax.swing.DefaultRowSorter; import javax.swing.SortOrder; import org.hdesktop.swingx.renderer.StringValue; import org.hdesktop.swingx.renderer.StringValues; import org.hdesktop.swingx.util.Contract; /** * A default SortController implementation used as parent class for concrete * SortControllers in SwingX.<p> * * Additionally, this implementation contains a fix for core * <a href=http://bugs.sun.com/bugdatabase/view_bug.do?bug_id=6894632>Issue 6894632</a>. * It guarantees to only touch the underlying model during sort/filter and during * processing the notification methods. This implies that the conversion and size query * methods are valid at all times outside the internal updates, including the critical * period (in core with undefined behaviour) after the underlying model has changed and * before this sorter has been notified. * * @author Jeanette Winzenburg */ public abstract class DefaultSortController<M> extends DefaultRowSorter<M, Integer> implements SortController<M> { /** * Comparator that uses compareTo on the contents. */ @SuppressWarnings("unchecked") public static final Comparator COMPARABLE_COMPARATOR = new ComparableComparator(); private final static SortOrder[] DEFAULT_CYCLE = new SortOrder[] {SortOrder.ASCENDING, SortOrder.DESCENDING}; private List<SortOrder> sortCycle; private boolean sortable; private StringValueProvider stringValueProvider; protected int cachedModelRowCount; public DefaultSortController() { super(); setSortable(true); setSortOrderCycle(DEFAULT_CYCLE); setSortsOnUpdates(true); } /** * {@inheritDoc} <p> * */ @Override public void setSortable(boolean sortable) { this.sortable = sortable; } /** * {@inheritDoc} <p> * */ @Override public boolean isSortable() { return sortable; } /** * {@inheritDoc} <p> * */ @Override public void setSortable(int column, boolean sortable) { super.setSortable(column, sortable); } /** * {@inheritDoc} <p> * */ @Override public boolean isSortable(int column) { if (!isSortable()) return false; return super.isSortable(column); } /** * {@inheritDoc} * <p> * * Overridden - that is completely new implementation - to get first/next SortOrder * from sort order cycle. Does nothing if the cycle is empty. */ @Override public void toggleSortOrder(int column) { checkColumn(column); if (!isSortable(column)) return; SortOrder firstInCycle = getFirstInCycle(); // nothing to toggle through if (firstInCycle == null) return; List<SortKey> keys = new ArrayList<SortKey>(getSortKeys()); SortKey sortKey = SortUtils.getFirstSortKeyForColumn(keys, column); if (keys.indexOf(sortKey) == 0) { // primary key: in this case we'll use next sortorder in cylce keys.set(0, new SortKey(column, getNextInCycle(sortKey.getSortOrder()))); } else { // all others: make primary with first sortOrder in cycle keys.remove(sortKey); keys.add(0, new SortKey(column, getFirstInCycle())); } if (keys.size() > getMaxSortKeys()) { keys = keys.subList(0, getMaxSortKeys()); } setSortKeys(keys); } /** * Returns the next SortOrder relative to the current, or null * if the sort order cycle is empty. * * @param current the current SortOrder * @return the next SortOrder to use, may be null if the cycle is empty. */ private SortOrder getNextInCycle(SortOrder current) { int pos = sortCycle.indexOf(current); if (pos < 0) { // not in cycle ... what to do? return getFirstInCycle(); } pos++; if (pos >= sortCycle.size()) { pos = 0; } return sortCycle.get(pos); } /** * Returns the first SortOrder in the sort order cycle, or null if empty. * * @return the first SortOrder in the sort order cycle or null if empty. */ private SortOrder getFirstInCycle() { return sortCycle.size() > 0 ? sortCycle.get(0) : null; } private void checkColumn(int column) { if (column < 0 || column >= getModelWrapper().getColumnCount()) { throw new IndexOutOfBoundsException( "column beyond range of TableModel"); } } /** * {@inheritDoc} <p> * * PENDING JW: toggle has two effects: makes the column the primary sort column, * and cycle through. So here we something similar. Should we? * */ @Override public void setSortOrder(int column, SortOrder sortOrder) { if (!isSortable(column)) return; SortKey replace = new SortKey(column, sortOrder); List<SortKey> keys = new ArrayList<SortKey>(getSortKeys()); SortUtils.removeFirstSortKeyForColumn(keys, column); keys.add(0, replace); // PENDING max sort keys, respect here? setSortKeys(keys); } /** * {@inheritDoc} <p> * */ @Override public SortOrder getSortOrder(int column) { SortKey key = SortUtils.getFirstSortKeyForColumn(getSortKeys(), column); return key != null ? key.getSortOrder() : SortOrder.UNSORTED; } /** * {@inheritDoc} <p> * */ @Override public void resetSortOrders() { if (!isSortable()) return; List<SortKey> keys = new ArrayList<SortKey>(getSortKeys()); for (int i = keys.size() -1; i >= 0; i--) { SortKey sortKey = keys.get(i); if (isSortable(sortKey.getColumn())) { keys.remove(sortKey); } } setSortKeys(keys); } /** * {@inheritDoc} <p> */ @Override public SortOrder[] getSortOrderCycle() { return sortCycle.toArray(new SortOrder[0]); } /** * {@inheritDoc} <p> */ @Override public void setSortOrderCycle(SortOrder... cycle) { Contract.asNotNull(cycle, "Elements of SortOrderCycle must not be null"); // JW: not safe enough? sortCycle = Arrays.asList(cycle); } /** * Sets the registry of string values. If null, the default provider is used. * * @param registry the registry to get StringValues for conversion. */ @Override public void setStringValueProvider(StringValueProvider registry) { this.stringValueProvider = registry; // updateStringConverter(); } /** * Returns the registry of string values. * * @return the registry of string converters, guaranteed to never be null. */ @Override public StringValueProvider getStringValueProvider() { if (stringValueProvider == null) { stringValueProvider = DEFAULT_PROVIDER; } return stringValueProvider; } /** * Returns the default cycle. * * @return default sort order cycle. */ public static SortOrder[] getDefaultSortOrderCycle() { return Arrays.copyOf(DEFAULT_CYCLE, DEFAULT_CYCLE.length); } private static final StringValueProvider DEFAULT_PROVIDER = new StringValueProvider() { @Override public StringValue getStringValue(int row, int column) { return StringValues.TO_STRING; } }; @SuppressWarnings("unchecked") private static class ComparableComparator implements Comparator { public int compare(Object o1, Object o2) { return ((Comparable)o1).compareTo(o2); } } //-------------------------- replacing super for more consistent conversion/rowCount behaviour /** * {@inheritDoc} <p> * * Overridden to use check against <code>getViewRowCount</code> for validity. * * @see #getViewRowCount() */ @Override public int convertRowIndexToModel(int viewIndex) { if ((viewIndex < 0) || viewIndex >= getViewRowCount()) throw new IndexOutOfBoundsException("valid viewIndex: 0 <= index < " + getViewRowCount() + " but was: " + viewIndex); try { return super.convertRowIndexToModel(viewIndex); } catch (Exception e) { // this will happen only if unsorted/-filtered and super // incorrectly access the model while it had been changed // under its feet } return viewIndex; } /** * {@inheritDoc} <p> * * Overridden to use check against <code>getModelRowCount</code> for validity. * * @see #getModelRowCount() */ @Override public int convertRowIndexToView(int modelIndex) { if ((modelIndex < 0) || modelIndex >= getModelRowCount()) throw new IndexOutOfBoundsException("valid modelIndex: 0 <= index < " + getModelRowCount() + " but was: " + modelIndex); try { return super.convertRowIndexToView(modelIndex); } catch (Exception e) { // this will happen only if unsorted/-filtered and super // incorrectly access the model while it had been changed // under its feet } return modelIndex; } /** * {@inheritDoc} <p> * * Overridden to return the model row count which corresponds to the currently * mapped model instead of accessing the model directly (as super does). * This may differ from the "real" current model row count if the model has changed * but this sorter not yet notified. * */ @Override public int getModelRowCount() { return cachedModelRowCount; } /** * {@inheritDoc} <p> * * Overridden to return the model row count if no filters installed, otherwise * return super. * * @see #getModelRowCount() * */ @Override public int getViewRowCount() { if (hasRowFilter()) return super.getViewRowCount(); return getModelRowCount(); } /** * @return */ private boolean hasRowFilter() { return getRowFilter() != null; } //------------------ overridden notification methods: cache model row count @Override public void allRowsChanged() { cachedModelRowCount = getModelWrapper().getRowCount(); super.allRowsChanged(); } @Override public void modelStructureChanged() { super.modelStructureChanged(); cachedModelRowCount = getModelWrapper().getRowCount(); } @Override public void rowsDeleted(int firstRow, int endRow) { cachedModelRowCount = getModelWrapper().getRowCount(); super.rowsDeleted(firstRow, endRow); } @Override public void rowsInserted(int firstRow, int endRow) { cachedModelRowCount = getModelWrapper().getRowCount(); super.rowsInserted(firstRow, endRow); } }