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.plaf.basic.core; 023 024import javax.swing.DefaultListSelectionModel; 025import javax.swing.ListModel; 026import javax.swing.ListSelectionModel; 027import javax.swing.RowSorter; 028import javax.swing.event.ListDataEvent; 029import javax.swing.event.ListSelectionEvent; 030import javax.swing.event.RowSorterEvent; 031import javax.swing.event.RowSorterListener; 032 033import org.jdesktop.swingx.JXList; 034import org.jdesktop.swingx.SwingXUtilities; 035import org.jdesktop.swingx.util.Contract; 036 037//import sun.swing.SwingUtilities2; 038 039/** 040 * ListSortUI provides support for managing the synchronization between 041 * RowSorter, SelectionModel and ListModel if a JXList is sortable.<p> 042 * 043 * This implementation is an adaption of JTable.SortManager fit to the 044 * needs of a ListUI. In contrast to JTable tradition, the ui delegate has 045 * full control about listening to model/selection changes and updating 046 * the list accordingly. So it's role is that of a helper to the ui-delgate 047 * (vs. as a helper of the JTable). It's still up to the ListUI itself to 048 * listen to model/selection and propagate the notification to this class, if 049 * a sorter is installed, but still do the usual updates (layout, repaint) itself. 050 * On the other hand, listening to the sorter and updating list state accordingly 051 * is completely done by this. 052 * 053 */ 054public final class ListSortUI { 055 private RowSorter<? extends ListModel> sorter; 056 private JXList list; 057 058 // Selection, in terms of the model. This is lazily created 059 // as needed. 060 private ListSelectionModel modelSelection; 061 private int modelLeadIndex; 062 // Set to true while in the process of changing the selection. 063 // If this is true the selection change is ignored. 064 private boolean syncingSelection; 065 // Temporary cache of selection, in terms of model. This is only used 066 // if we don't need the full weight of modelSelection. 067 private int[] lastModelSelection; 068 private boolean sorterChanged; 069 private boolean ignoreSortChange; 070 private RowSorterListener sorterListener; 071 072 /** 073 * Intanstiates a SortUI on the list which has the given RowSorter. 074 * 075 * @param list the list to control, must not be null 076 * @param sorter the rowSorter of the list, must not be null 077 * @throws NullPointerException if either the list or the sorter is null 078 * @throws IllegalStateException if the sorter is not the sorter installed 079 * on the list 080 */ 081 public ListSortUI(JXList list, RowSorter<? extends ListModel> sorter) { 082 this.sorter = Contract.asNotNull(sorter, "RowSorter must not be null"); 083 this.list = Contract.asNotNull(list, "list must not be null"); 084 if (sorter != list.getRowSorter()) throw 085 new IllegalStateException("sorter must be same as the one on list"); 086 sorterListener = createRowSorterListener(); 087 sorter.addRowSorterListener(sorterListener); 088 } 089 090 /** 091 * Disposes any resources used by this SortManager. 092 * Note: this instance must not be used after dispose! 093 */ 094 public void dispose() { 095 if (sorter != null) { 096 sorter.removeRowSorterListener(sorterListener); 097 } 098 sorter = null; 099 list = null; 100 } 101 102//----------------------methods called by listeners 103 104 /** 105 * Called after notification from ListModel. 106 * @param e the change event from the listModel. 107 */ 108 public void modelChanged(ListDataEvent e) { 109 ModelChange change = new ModelChange(e); 110 prepareForChange(change); 111 notifySorter(change); 112 if (change.type != ListDataEvent.CONTENTS_CHANGED) { 113 // If the Sorter is unsorted we will not have received 114 // notification, force treating insert/delete as a change. 115 sorterChanged = true; 116 } 117 processChange(change); 118 } 119 120 /** 121 * Called after notification from selectionModel. 122 * 123 * Invoked when the selection, on the view, has changed. 124 */ 125 public void viewSelectionChanged(ListSelectionEvent e) { 126 if (!syncingSelection && modelSelection != null) { 127 modelSelection = null; 128 } 129 } 130 131 /** 132 * Called after notification from RowSorter. 133 * 134 * @param e RowSorter event of type SORTED. 135 */ 136 protected void sortedChanged(RowSorterEvent e) { 137 sorterChanged = true; 138 if (!ignoreSortChange) { 139 prepareForChange(e); 140 processChange(null); 141 // PENDING Jw: this is fix of 1161-swingx - not updated after setting 142 // rowFilter 143 // potentially costly? but how to distinguish a mere sort from a 144 // filterchanged? (only the latter requires a revalidate) 145 // first fix had only revalidate/repaint but was not 146 // good enough, see #1261-swingx - no items visible 147 // after setting rowFilter 148 // need to invalidate the cell size cache which might be needed 149 // even after plain sorting as the indi-sizes are now at different 150 // positions 151 list.invalidateCellSizeCache(); 152 } 153 } 154 155 156//--------------------- prepare change, that is cache selection if needed 157 /** 158 * Invoked when the RowSorter has changed. 159 * Updates the internal cache of the selection based on the change. 160 * 161 * @param sortEvent the notification 162 * @throws NullPointerException if the given event is null. 163 */ 164 private void prepareForChange(RowSorterEvent sortEvent) { 165 Contract.asNotNull(sortEvent, "sorter event not null"); 166 // sort order changed. If modelSelection is null and filtering 167 // is enabled we need to cache the selection in terms of the 168 // underlying model, this will allow us to correctly restore 169 // the selection even if rows are filtered out. 170 if (modelSelection == null && 171 sorter.getViewRowCount() != sorter.getModelRowCount()) { 172 modelSelection = new DefaultListSelectionModel(); 173 ListSelectionModel viewSelection = getViewSelectionModel(); 174 int min = viewSelection.getMinSelectionIndex(); 175 int max = viewSelection.getMaxSelectionIndex(); 176 int modelIndex; 177 for (int viewIndex = min; viewIndex <= max; viewIndex++) { 178 if (viewSelection.isSelectedIndex(viewIndex)) { 179 modelIndex = convertRowIndexToModel( 180 sortEvent, viewIndex); 181 if (modelIndex != -1) { 182 modelSelection.addSelectionInterval( 183 modelIndex, modelIndex); 184 } 185 } 186 } 187 modelIndex = convertRowIndexToModel(sortEvent, 188 viewSelection.getLeadSelectionIndex()); 189 SwingXUtilities.setLeadAnchorWithoutSelection( 190 modelSelection, modelIndex, modelIndex); 191 } else if (modelSelection == null) { 192 // Sorting changed, haven't cached selection in terms 193 // of model and no filtering. Temporarily cache selection. 194 cacheModelSelection(sortEvent); 195 } 196 } 197 /** 198 * Invoked when the list model has changed. This is invoked prior to 199 * notifying the sorter of the change. 200 * Updates the internal cache of the selection based on the change. 201 * 202 * @param change the notification 203 * @throws NullPointerException if the given event is null. 204 */ 205 private void prepareForChange(ModelChange change) { 206 Contract.asNotNull(change, "table event not null"); 207 if (change.allRowsChanged) { 208 // All the rows have changed, chuck any cached selection. 209 modelSelection = null; 210 } else if (modelSelection != null) { 211 // Table changed, reflect changes in cached selection model. 212 switch (change.type) { 213 case ListDataEvent.INTERVAL_REMOVED: 214 modelSelection.removeIndexInterval(change.startModelIndex, 215 change.endModelIndex); 216 break; 217 case ListDataEvent.INTERVAL_ADDED: 218 modelSelection.insertIndexInterval(change.startModelIndex, 219 change.endModelIndex, true); 220 break; 221 default: 222 break; 223 } 224 } else { 225 // table changed, but haven't cached rows, temporarily 226 // cache them. 227 cacheModelSelection(null); 228 } 229 } 230 231 232 233 private void cacheModelSelection(RowSorterEvent sortEvent) { 234 lastModelSelection = convertSelectionToModel(sortEvent); 235 modelLeadIndex = convertRowIndexToModel(sortEvent, 236 getViewSelectionModel().getLeadSelectionIndex()); 237 } 238 239//----------------------- process change, that is restore selection if needed 240 /** 241 * Inovked when either the table has changed or the sorter has changed 242 * and after the sorter has been notified. If necessary this will 243 * reapply the selection and variable row heights. 244 */ 245 private void processChange(ModelChange change) { 246 if (change != null && change.allRowsChanged) { 247 allChanged(); 248 getViewSelectionModel().clearSelection(); 249 } else if (sorterChanged) { 250 restoreSelection(change); 251 } 252 } 253 254 /** 255 * Restores the selection from that in terms of the model. 256 */ 257 private void restoreSelection(ModelChange change) { 258 syncingSelection = true; 259 if (lastModelSelection != null) { 260 restoreSortingSelection(lastModelSelection, 261 modelLeadIndex, change); 262 lastModelSelection = null; 263 } else if (modelSelection != null) { 264 ListSelectionModel viewSelection = getViewSelectionModel(); 265 viewSelection.setValueIsAdjusting(true); 266 viewSelection.clearSelection(); 267 int min = modelSelection.getMinSelectionIndex(); 268 int max = modelSelection.getMaxSelectionIndex(); 269 int viewIndex; 270 for (int modelIndex = min; modelIndex <= max; modelIndex++) { 271 if (modelSelection.isSelectedIndex(modelIndex)) { 272 viewIndex = sorter.convertRowIndexToView(modelIndex); 273 if (viewIndex != -1) { 274 viewSelection.addSelectionInterval(viewIndex, 275 viewIndex); 276 } 277 } 278 } 279 // Restore the lead 280 int viewLeadIndex = modelSelection.getLeadSelectionIndex(); 281 if (viewLeadIndex != -1) { 282 viewLeadIndex = sorter.convertRowIndexToView(viewLeadIndex); 283 } 284 SwingXUtilities.setLeadAnchorWithoutSelection( 285 viewSelection, viewLeadIndex, viewLeadIndex); 286 viewSelection.setValueIsAdjusting(false); 287 } 288 syncingSelection = false; 289 } 290 291 /** 292 * Restores the selection after a model event/sort order changes. 293 * All coordinates are in terms of the model. 294 */ 295 private void restoreSortingSelection(int[] selection, int lead, 296 ModelChange change) { 297 // Convert the selection from model to view 298 for (int i = selection.length - 1; i >= 0; i--) { 299 selection[i] = convertRowIndexToView(change, selection[i]); 300 } 301 lead = convertRowIndexToView(change, lead); 302 303 // Check for the common case of no change in selection for 1 row 304 if (selection.length == 0 || 305 (selection.length == 1 && selection[0] == list.getSelectedIndex())) { 306 return; 307 } 308 ListSelectionModel selectionModel = getViewSelectionModel(); 309 // And apply the new selection 310 selectionModel.setValueIsAdjusting(true); 311 selectionModel.clearSelection(); 312 for (int i = selection.length - 1; i >= 0; i--) { 313 if (selection[i] != -1) { 314 selectionModel.addSelectionInterval(selection[i], 315 selection[i]); 316 } 317 } 318 SwingXUtilities.setLeadAnchorWithoutSelection( 319 selectionModel, lead, lead); 320 selectionModel.setValueIsAdjusting(false); 321 } 322 323//------------------- row index conversion methods 324 /** 325 * Converts a model index to view index. This is called when the 326 * sorter or model changes and sorting is enabled. 327 * 328 * @param change describes the TableModelEvent that initiated the change; 329 * will be null if called as the result of a sort 330 */ 331 private int convertRowIndexToView(ModelChange change, int modelIndex) { 332 if (modelIndex < 0) { 333 return -1; 334 } 335// Contract.asNotNull(change, "change must not be null?"); 336 if (change != null && modelIndex >= change.startModelIndex) { 337 if (change.type == ListDataEvent.INTERVAL_ADDED) { 338 if (modelIndex + change.length >= change.modelRowCount) { 339 return -1; 340 } 341 return sorter.convertRowIndexToView( 342 modelIndex + change.length); 343 } 344 else if (change.type == ListDataEvent.INTERVAL_REMOVED) { 345 if (modelIndex <= change.endModelIndex) { 346 // deleted 347 return -1; 348 } 349 else { 350 if (modelIndex - change.length >= change.modelRowCount) { 351 return -1; 352 } 353 return sorter.convertRowIndexToView( 354 modelIndex - change.length); 355 } 356 } 357 // else, updated 358 } 359 if (modelIndex >= sorter.getModelRowCount()) { 360 return -1; 361 } 362 return sorter.convertRowIndexToView(modelIndex); 363 } 364 365 366 private int convertRowIndexToModel(RowSorterEvent e, int viewIndex) { 367 // JW: the event is null if the selection is cached in prepareChange 368 // after model notification. Then the conversion from the 369 // sorter is still valid as the prepare is called before 370 // notifying the sorter. 371 if (e != null) { 372 if (e.getPreviousRowCount() == 0) { 373 return viewIndex; 374 } 375 // range checking handled by RowSorterEvent 376 return e.convertPreviousRowIndexToModel(viewIndex); 377 } 378 // Make sure the viewIndex is valid 379 if (viewIndex < 0 || viewIndex >= sorter.getViewRowCount()) { 380 return -1; 381 } 382 return sorter.convertRowIndexToModel(viewIndex); 383 } 384 385 /** 386 * Converts the selection to model coordinates. This is used when 387 * the model changes or the sorter changes. 388 */ 389 private int[] convertSelectionToModel(RowSorterEvent e) { 390 int[] selection = list.getSelectedIndices(); 391 for (int i = selection.length - 1; i >= 0; i--) { 392 selection[i] = convertRowIndexToModel(e, selection[i]); 393 } 394 return selection; 395 } 396 397//------------------ 398 /** 399 * Notifies the sorter of a change in the underlying model. 400 */ 401 private void notifySorter(ModelChange change) { 402 try { 403 ignoreSortChange = true; 404 sorterChanged = false; 405 if (change.allRowsChanged) { 406 sorter.allRowsChanged(); 407 } else { 408 switch (change.type) { 409 case ListDataEvent.CONTENTS_CHANGED: 410 sorter.rowsUpdated(change.startModelIndex, 411 change.endModelIndex); 412 break; 413 case ListDataEvent.INTERVAL_ADDED: 414 sorter.rowsInserted(change.startModelIndex, 415 change.endModelIndex); 416 break; 417 case ListDataEvent.INTERVAL_REMOVED: 418 sorter.rowsDeleted(change.startModelIndex, 419 change.endModelIndex); 420 break; 421 } 422 } 423 } finally { 424 ignoreSortChange = false; 425 } 426 } 427 428 429 private ListSelectionModel getViewSelectionModel() { 430 return list.getSelectionModel(); 431 } 432 /** 433 * Invoked when the underlying model has completely changed. 434 */ 435 private void allChanged() { 436 modelLeadIndex = -1; 437 modelSelection = null; 438 } 439 440//------------------- implementing listeners 441 442 /** 443 * Creates and returns a RowSorterListener. This implementation 444 * calls sortedChanged if the event is of type SORTED. 445 * 446 * @return rowSorterListener to install on sorter. 447 */ 448 protected RowSorterListener createRowSorterListener() { 449 RowSorterListener l = new RowSorterListener() { 450 451 @Override 452 public void sorterChanged(RowSorterEvent e) { 453 if (e.getType() == RowSorterEvent.Type.SORTED) { 454 sortedChanged(e); 455 } 456 457 } 458 459 }; 460 return l; 461 } 462 /** 463 * ModelChange is used when sorting to restore state, it corresponds 464 * to data from a TableModelEvent. The values are precalculated as 465 * they are used extensively.<p> 466 * 467 * PENDING JW: this is not yet fully adapted to ListDataEvent. 468 */ 469 final static class ModelChange { 470 // JW: if we received a dataChanged, there _is no_ notion 471 // of end/start/length of change 472 // Starting index of the change, in terms of the model, -1 if dataChanged 473 int startModelIndex; 474 475 // Ending index of the change, in terms of the model, -1 if dataChanged 476 int endModelIndex; 477 478 // Length of the change (end - start + 1), - 1 if dataChanged 479 int length; 480 481 // Type of change 482 int type; 483 484 // Number of rows in the model 485 int modelRowCount; 486 487 488 // True if the event indicates all the contents have changed 489 boolean allRowsChanged; 490 491 public ModelChange(ListDataEvent e) { 492 type = e.getType(); 493 modelRowCount = ((ListModel) e.getSource()).getSize(); 494 startModelIndex = e.getIndex0(); 495 endModelIndex = e.getIndex1(); 496 allRowsChanged = startModelIndex < 0; 497 length = allRowsChanged ? -1 : endModelIndex - startModelIndex + 1; 498 } 499 } 500 501 502} 503