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