001/* 002 * $Id$ 003 * 004 * Copyright 2009 Sun Microsystems, Inc., 4150 Network Circle, 005 * Santa Clara, California 95054, U.S.A. All rights reserved. 006 * 007 * This library is free software; you can redistribute it and/or 008 * modify it under the terms of the GNU Lesser General Public 009 * License as published by the Free Software Foundation; either 010 * version 2.1 of the License, or (at your option) any later version. 011 * 012 * This library is distributed in the hope that it will be useful, 013 * but WITHOUT ANY WARRANTY; without even the implied warranty of 014 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU 015 * Lesser General Public License for more details. 016 * 017 * You should have received a copy of the GNU Lesser General Public 018 * License along with this library; if not, write to the Free Software 019 * Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA 020 * 021 */ 022package org.jdesktop.swingx.sort; 023 024import java.util.ArrayList; 025import java.util.Arrays; 026import java.util.Comparator; 027import java.util.List; 028 029import javax.swing.DefaultRowSorter; 030import javax.swing.SortOrder; 031 032import org.jdesktop.swingx.renderer.StringValue; 033import org.jdesktop.swingx.renderer.StringValues; 034import org.jdesktop.swingx.util.Contract; 035 036/** 037 * A default SortController implementation used as parent class for concrete 038 * SortControllers in SwingX.<p> 039 * 040 * Additionally, this implementation contains a fix for core 041 * <a href=http://bugs.sun.com/bugdatabase/view_bug.do?bug_id=6894632>Issue 6894632</a>. 042 * It guarantees to only touch the underlying model during sort/filter and during 043 * processing the notification methods. This implies that the conversion and size query 044 * methods are valid at all times outside the internal updates, including the critical 045 * period (in core with undefined behaviour) after the underlying model has changed and 046 * before this sorter has been notified. 047 * 048 * @author Jeanette Winzenburg 049 */ 050public abstract class DefaultSortController<M> extends DefaultRowSorter<M, Integer> implements 051 SortController<M> { 052 053 /** 054 * Comparator that uses compareTo on the contents. 055 */ 056 @SuppressWarnings({ "unchecked", "rawtypes" }) 057 public static final Comparator COMPARABLE_COMPARATOR = 058 new ComparableComparator(); 059 060 private final static SortOrder[] DEFAULT_CYCLE = new SortOrder[] {SortOrder.ASCENDING, SortOrder.DESCENDING}; 061 062 private List<SortOrder> sortCycle; 063 064 private boolean sortable; 065 066 private StringValueProvider stringValueProvider; 067 068 protected int cachedModelRowCount; 069 070 public DefaultSortController() { 071 super(); 072 setSortable(true); 073 setSortOrderCycle(DEFAULT_CYCLE); 074 setSortsOnUpdates(true); 075 } 076 /** 077 * {@inheritDoc} <p> 078 * 079 */ 080 @Override 081 public void setSortable(boolean sortable) { 082 this.sortable = sortable; 083 } 084 085 /** 086 * {@inheritDoc} <p> 087 * 088 */ 089 @Override 090 public boolean isSortable() { 091 return sortable; 092 } 093 094 /** 095 * {@inheritDoc} <p> 096 * 097 */ 098 @Override 099 public void setSortable(int column, boolean sortable) { 100 super.setSortable(column, sortable); 101 } 102 103 /** 104 * {@inheritDoc} <p> 105 * 106 */ 107 @Override 108 public boolean isSortable(int column) { 109 if (!isSortable()) return false; 110 return super.isSortable(column); 111 } 112 113 /** 114 * {@inheritDoc} 115 * <p> 116 * 117 * Overridden - that is completely new implementation - to get first/next SortOrder 118 * from sort order cycle. Does nothing if the cycle is empty. 119 */ 120 @Override 121 public void toggleSortOrder(int column) { 122 checkColumn(column); 123 if (!isSortable(column)) 124 return; 125 SortOrder firstInCycle = getFirstInCycle(); 126 // nothing to toggle through 127 if (firstInCycle == null) 128 return; 129 List<SortKey> keys = new ArrayList<SortKey>(getSortKeys()); 130 SortKey sortKey = SortUtils.getFirstSortKeyForColumn(keys, column); 131 if (keys.indexOf(sortKey) == 0) { 132 // primary key: in this case we'll use next sortorder in cylce 133 keys.set(0, new SortKey(column, getNextInCycle(sortKey.getSortOrder()))); 134 } else { 135 // all others: make primary with first sortOrder in cycle 136 keys.remove(sortKey); 137 keys.add(0, new SortKey(column, getFirstInCycle())); 138 } 139 if (keys.size() > getMaxSortKeys()) { 140 keys = keys.subList(0, getMaxSortKeys()); 141 } 142 setSortKeys(keys); 143 } 144 145 146 /** 147 * Returns the next SortOrder relative to the current, or null 148 * if the sort order cycle is empty. 149 * 150 * @param current the current SortOrder 151 * @return the next SortOrder to use, may be null if the cycle is empty. 152 */ 153 private SortOrder getNextInCycle(SortOrder current) { 154 int pos = sortCycle.indexOf(current); 155 if (pos < 0) { 156 // not in cycle ... what to do? 157 return getFirstInCycle(); 158 } 159 pos++; 160 if (pos >= sortCycle.size()) { 161 pos = 0; 162 } 163 return sortCycle.get(pos); 164 } 165 166 /** 167 * Returns the first SortOrder in the sort order cycle, or null if empty. 168 * 169 * @return the first SortOrder in the sort order cycle or null if empty. 170 */ 171 private SortOrder getFirstInCycle() { 172 return sortCycle.size() > 0 ? sortCycle.get(0) : null; 173 } 174 175 private void checkColumn(int column) { 176 if (column < 0 || column >= getModelWrapper().getColumnCount()) { 177 throw new IndexOutOfBoundsException( 178 "column beyond range of TableModel"); 179 } 180 } 181 182 /** 183 * {@inheritDoc} <p> 184 * 185 * PENDING JW: toggle has two effects: makes the column the primary sort column, 186 * and cycle through. So here we something similar. Should we? 187 * 188 */ 189 @Override 190 public void setSortOrder(int column, SortOrder sortOrder) { 191 if (!isSortable(column)) return; 192 SortKey replace = new SortKey(column, sortOrder); 193 List<SortKey> keys = new ArrayList<SortKey>(getSortKeys()); 194 SortUtils.removeFirstSortKeyForColumn(keys, column); 195 keys.add(0, replace); 196 // PENDING max sort keys, respect here? 197 setSortKeys(keys); 198 } 199 200 /** 201 * {@inheritDoc} <p> 202 * 203 */ 204 @Override 205 public SortOrder getSortOrder(int column) { 206 SortKey key = SortUtils.getFirstSortKeyForColumn(getSortKeys(), column); 207 return key != null ? key.getSortOrder() : SortOrder.UNSORTED; 208 } 209 210 /** 211 * {@inheritDoc} <p> 212 * 213 */ 214 @Override 215 public void resetSortOrders() { 216 if (!isSortable()) return; 217 List<SortKey> keys = new ArrayList<SortKey>(getSortKeys()); 218 for (int i = keys.size() -1; i >= 0; i--) { 219 SortKey sortKey = keys.get(i); 220 if (isSortable(sortKey.getColumn())) { 221 keys.remove(sortKey); 222 } 223 224 } 225 setSortKeys(keys); 226 227 } 228 229 230 /** 231 * {@inheritDoc} <p> 232 */ 233 @Override 234 public SortOrder[] getSortOrderCycle() { 235 return sortCycle.toArray(new SortOrder[0]); 236 } 237 238 /** 239 * {@inheritDoc} <p> 240 */ 241 @Override 242 public void setSortOrderCycle(SortOrder... cycle) { 243 Contract.asNotNull(cycle, "Elements of SortOrderCycle must not be null"); 244 // JW: not safe enough? 245 sortCycle = Arrays.asList(cycle); 246 } 247 248 /** 249 * Sets the registry of string values. If null, the default provider is used. 250 * 251 * @param registry the registry to get StringValues for conversion. 252 */ 253 @Override 254 public void setStringValueProvider(StringValueProvider registry) { 255 this.stringValueProvider = registry; 256// updateStringConverter(); 257 } 258 259 /** 260 * Returns the registry of string values. 261 * 262 * @return the registry of string converters, guaranteed to never be null. 263 */ 264 @Override 265 public StringValueProvider getStringValueProvider() { 266 if (stringValueProvider == null) { 267 stringValueProvider = DEFAULT_PROVIDER; 268 } 269 return stringValueProvider; 270 } 271 272 /** 273 * Returns the default cycle. 274 * 275 * @return default sort order cycle. 276 */ 277 public static SortOrder[] getDefaultSortOrderCycle() { 278 return Arrays.copyOf(DEFAULT_CYCLE, DEFAULT_CYCLE.length); 279 } 280 281 private static final StringValueProvider DEFAULT_PROVIDER = new StringValueProvider() { 282 283 @Override 284 public StringValue getStringValue(int row, int column) { 285 return StringValues.TO_STRING; 286 } 287 288 }; 289 290 291 @SuppressWarnings({ "unchecked", "rawtypes" }) 292 private static class ComparableComparator implements Comparator { 293 @Override 294 public int compare(Object o1, Object o2) { 295 return ((Comparable)o1).compareTo(o2); 296 } 297 } 298 299//-------------------------- replacing super for more consistent conversion/rowCount behaviour 300 301 /** 302 * {@inheritDoc} <p> 303 * 304 * Overridden to use check against <code>getViewRowCount</code> for validity. 305 * 306 * @see #getViewRowCount() 307 */ 308 @Override 309 public int convertRowIndexToModel(int viewIndex) { 310 if ((viewIndex < 0) || viewIndex >= getViewRowCount()) 311 throw new IndexOutOfBoundsException("valid viewIndex: 0 <= index < " 312 + getViewRowCount() 313 + " but was: " + viewIndex); 314 try { 315 return super.convertRowIndexToModel(viewIndex); 316 } catch (Exception e) { 317 // this will happen only if unsorted/-filtered and super 318 // incorrectly access the model while it had been changed 319 // under its feet 320 } 321 return viewIndex; 322 } 323 324 325 /** 326 * {@inheritDoc} <p> 327 * 328 * Overridden to use check against <code>getModelRowCount</code> for validity. 329 * 330 * @see #getModelRowCount() 331 */ 332 @Override 333 public int convertRowIndexToView(int modelIndex) { 334 if ((modelIndex < 0) || modelIndex >= getModelRowCount()) 335 throw new IndexOutOfBoundsException("valid modelIndex: 0 <= index < " 336 + getModelRowCount() 337 + " but was: " + modelIndex); 338 try { 339 return super.convertRowIndexToView(modelIndex); 340 } catch (Exception e) { 341 // this will happen only if unsorted/-filtered and super 342 // incorrectly access the model while it had been changed 343 // under its feet 344 } 345 return modelIndex; 346 } 347 348 /** 349 * {@inheritDoc} <p> 350 * 351 * Overridden to return the model row count which corresponds to the currently 352 * mapped model instead of accessing the model directly (as super does). 353 * This may differ from the "real" current model row count if the model has changed 354 * but this sorter not yet notified. 355 * 356 */ 357 @Override 358 public int getModelRowCount() { 359 return cachedModelRowCount; 360 } 361 362 /** 363 * {@inheritDoc} <p> 364 * 365 * Overridden to return the model row count if no filters installed, otherwise 366 * return super. 367 * 368 * @see #getModelRowCount() 369 * 370 */ 371 @Override 372 public int getViewRowCount() { 373 if (hasRowFilter()) 374 return super.getViewRowCount(); 375 return getModelRowCount(); 376 } 377 378 /** 379 * @return 380 */ 381 private boolean hasRowFilter() { 382 return getRowFilter() != null; 383 } 384 385//------------------ overridden notification methods: cache model row count 386 @Override 387 public void allRowsChanged() { 388 cachedModelRowCount = getModelWrapper().getRowCount(); 389 super.allRowsChanged(); 390 } 391 @Override 392 public void modelStructureChanged() { 393 super.modelStructureChanged(); 394 cachedModelRowCount = getModelWrapper().getRowCount(); 395 } 396 @Override 397 public void rowsDeleted(int firstRow, int endRow) { 398 cachedModelRowCount = getModelWrapper().getRowCount(); 399 super.rowsDeleted(firstRow, endRow); 400 } 401 @Override 402 public void rowsInserted(int firstRow, int endRow) { 403 cachedModelRowCount = getModelWrapper().getRowCount(); 404 super.rowsInserted(firstRow, endRow); 405 } 406 407}