All Downloads are FREE. Search and download functionalities are using the official Maven repository.

org.jdesktop.swingx.plaf.basic.core.ListSortUI Maven / Gradle / Ivy

The newest version!
/*
 * $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.jdesktop.swingx.plaf.basic.core;

import javax.swing.DefaultListSelectionModel;
import javax.swing.ListModel;
import javax.swing.ListSelectionModel;
import javax.swing.RowSorter;
import javax.swing.event.ListDataEvent;
import javax.swing.event.ListSelectionEvent;
import javax.swing.event.RowSorterEvent;
import javax.swing.event.RowSorterListener;

import org.jdesktop.swingx.JXList;
import org.jdesktop.swingx.SwingXUtilities;
import org.jdesktop.swingx.util.Contract;

//import sun.swing.SwingUtilities2;

/**
 * ListSortUI provides support for managing the synchronization between
 * RowSorter, SelectionModel and ListModel if a JXList is sortable.

* * This implementation is an adaption of JTable.SortManager fit to the * needs of a ListUI. In contrast to JTable tradition, the ui delegate has * full control about listening to model/selection changes and updating * the list accordingly. So it's role is that of a helper to the ui-delgate * (vs. as a helper of the JTable). It's still up to the ListUI itself to * listen to model/selection and propagate the notification to this class, if * a sorter is installed, but still do the usual updates (layout, repaint) itself. * On the other hand, listening to the sorter and updating list state accordingly * is completely done by this. * */ public final class ListSortUI { private RowSorter sorter; private JXList list; // Selection, in terms of the model. This is lazily created // as needed. private ListSelectionModel modelSelection; private int modelLeadIndex; // Set to true while in the process of changing the selection. // If this is true the selection change is ignored. private boolean syncingSelection; // Temporary cache of selection, in terms of model. This is only used // if we don't need the full weight of modelSelection. private int[] lastModelSelection; private boolean sorterChanged; private boolean ignoreSortChange; private RowSorterListener sorterListener; /** * Intanstiates a SortUI on the list which has the given RowSorter. * * @param list the list to control, must not be null * @param sorter the rowSorter of the list, must not be null * @throws NullPointerException if either the list or the sorter is null * @throws IllegalStateException if the sorter is not the sorter installed * on the list */ public ListSortUI(JXList list, RowSorter sorter) { this.sorter = Contract.asNotNull(sorter, "RowSorter must not be null"); this.list = Contract.asNotNull(list, "list must not be null"); if (sorter != list.getRowSorter()) throw new IllegalStateException("sorter must be same as the one on list"); sorterListener = createRowSorterListener(); sorter.addRowSorterListener(sorterListener); } /** * Disposes any resources used by this SortManager. * Note: this instance must not be used after dispose! */ public void dispose() { if (sorter != null) { sorter.removeRowSorterListener(sorterListener); } sorter = null; list = null; } //----------------------methods called by listeners /** * Called after notification from ListModel. * @param e the change event from the listModel. */ public void modelChanged(ListDataEvent e) { ModelChange change = new ModelChange(e); prepareForChange(change); notifySorter(change); if (change.type != ListDataEvent.CONTENTS_CHANGED) { // If the Sorter is unsorted we will not have received // notification, force treating insert/delete as a change. sorterChanged = true; } processChange(change); } /** * Called after notification from selectionModel. * * Invoked when the selection, on the view, has changed. */ public void viewSelectionChanged(ListSelectionEvent e) { if (!syncingSelection && modelSelection != null) { modelSelection = null; } } /** * Called after notification from RowSorter. * * @param e RowSorter event of type SORTED. */ protected void sortedChanged(RowSorterEvent e) { sorterChanged = true; if (!ignoreSortChange) { prepareForChange(e); processChange(null); // PENDING Jw: this is fix of 1161-swingx - not updated after setting // rowFilter // potentially costly? but how to distinguish a mere sort from a // filterchanged? (only the latter requires a revalidate) // first fix had only revalidate/repaint but was not // good enough, see #1261-swingx - no items visible // after setting rowFilter // need to invalidate the cell size cache which might be needed // even after plain sorting as the indi-sizes are now at different // positions list.invalidateCellSizeCache(); } } //--------------------- prepare change, that is cache selection if needed /** * Invoked when the RowSorter has changed. * Updates the internal cache of the selection based on the change. * * @param sortEvent the notification * @throws NullPointerException if the given event is null. */ private void prepareForChange(RowSorterEvent sortEvent) { Contract.asNotNull(sortEvent, "sorter event not null"); // sort order changed. If modelSelection is null and filtering // is enabled we need to cache the selection in terms of the // underlying model, this will allow us to correctly restore // the selection even if rows are filtered out. if (modelSelection == null && sorter.getViewRowCount() != sorter.getModelRowCount()) { modelSelection = new DefaultListSelectionModel(); ListSelectionModel viewSelection = getViewSelectionModel(); int min = viewSelection.getMinSelectionIndex(); int max = viewSelection.getMaxSelectionIndex(); int modelIndex; for (int viewIndex = min; viewIndex <= max; viewIndex++) { if (viewSelection.isSelectedIndex(viewIndex)) { modelIndex = convertRowIndexToModel( sortEvent, viewIndex); if (modelIndex != -1) { modelSelection.addSelectionInterval( modelIndex, modelIndex); } } } modelIndex = convertRowIndexToModel(sortEvent, viewSelection.getLeadSelectionIndex()); SwingXUtilities.setLeadAnchorWithoutSelection( modelSelection, modelIndex, modelIndex); } else if (modelSelection == null) { // Sorting changed, haven't cached selection in terms // of model and no filtering. Temporarily cache selection. cacheModelSelection(sortEvent); } } /** * Invoked when the list model has changed. This is invoked prior to * notifying the sorter of the change. * Updates the internal cache of the selection based on the change. * * @param change the notification * @throws NullPointerException if the given event is null. */ private void prepareForChange(ModelChange change) { Contract.asNotNull(change, "table event not null"); if (change.allRowsChanged) { // All the rows have changed, chuck any cached selection. modelSelection = null; } else if (modelSelection != null) { // Table changed, reflect changes in cached selection model. switch (change.type) { case ListDataEvent.INTERVAL_REMOVED: modelSelection.removeIndexInterval(change.startModelIndex, change.endModelIndex); break; case ListDataEvent.INTERVAL_ADDED: modelSelection.insertIndexInterval(change.startModelIndex, change.endModelIndex, true); break; default: break; } } else { // table changed, but haven't cached rows, temporarily // cache them. cacheModelSelection(null); } } private void cacheModelSelection(RowSorterEvent sortEvent) { lastModelSelection = convertSelectionToModel(sortEvent); modelLeadIndex = convertRowIndexToModel(sortEvent, getViewSelectionModel().getLeadSelectionIndex()); } //----------------------- process change, that is restore selection if needed /** * Inovked when either the table has changed or the sorter has changed * and after the sorter has been notified. If necessary this will * reapply the selection and variable row heights. */ private void processChange(ModelChange change) { if (change != null && change.allRowsChanged) { allChanged(); getViewSelectionModel().clearSelection(); } else if (sorterChanged) { restoreSelection(change); } } /** * Restores the selection from that in terms of the model. */ private void restoreSelection(ModelChange change) { syncingSelection = true; if (lastModelSelection != null) { restoreSortingSelection(lastModelSelection, modelLeadIndex, change); lastModelSelection = null; } else if (modelSelection != null) { ListSelectionModel viewSelection = getViewSelectionModel(); viewSelection.setValueIsAdjusting(true); viewSelection.clearSelection(); int min = modelSelection.getMinSelectionIndex(); int max = modelSelection.getMaxSelectionIndex(); int viewIndex; for (int modelIndex = min; modelIndex <= max; modelIndex++) { if (modelSelection.isSelectedIndex(modelIndex)) { viewIndex = sorter.convertRowIndexToView(modelIndex); if (viewIndex != -1) { viewSelection.addSelectionInterval(viewIndex, viewIndex); } } } // Restore the lead int viewLeadIndex = modelSelection.getLeadSelectionIndex(); if (viewLeadIndex != -1) { viewLeadIndex = sorter.convertRowIndexToView(viewLeadIndex); } SwingXUtilities.setLeadAnchorWithoutSelection( viewSelection, viewLeadIndex, viewLeadIndex); viewSelection.setValueIsAdjusting(false); } syncingSelection = false; } /** * Restores the selection after a model event/sort order changes. * All coordinates are in terms of the model. */ private void restoreSortingSelection(int[] selection, int lead, ModelChange change) { // Convert the selection from model to view for (int i = selection.length - 1; i >= 0; i--) { selection[i] = convertRowIndexToView(change, selection[i]); } lead = convertRowIndexToView(change, lead); // Check for the common case of no change in selection for 1 row if (selection.length == 0 || (selection.length == 1 && selection[0] == list.getSelectedIndex())) { return; } ListSelectionModel selectionModel = getViewSelectionModel(); // And apply the new selection selectionModel.setValueIsAdjusting(true); selectionModel.clearSelection(); for (int i = selection.length - 1; i >= 0; i--) { if (selection[i] != -1) { selectionModel.addSelectionInterval(selection[i], selection[i]); } } SwingXUtilities.setLeadAnchorWithoutSelection( selectionModel, lead, lead); selectionModel.setValueIsAdjusting(false); } //------------------- row index conversion methods /** * Converts a model index to view index. This is called when the * sorter or model changes and sorting is enabled. * * @param change describes the TableModelEvent that initiated the change; * will be null if called as the result of a sort */ private int convertRowIndexToView(ModelChange change, int modelIndex) { if (modelIndex < 0) { return -1; } // Contract.asNotNull(change, "change must not be null?"); if (change != null && modelIndex >= change.startModelIndex) { if (change.type == ListDataEvent.INTERVAL_ADDED) { if (modelIndex + change.length >= change.modelRowCount) { return -1; } return sorter.convertRowIndexToView( modelIndex + change.length); } else if (change.type == ListDataEvent.INTERVAL_REMOVED) { if (modelIndex <= change.endModelIndex) { // deleted return -1; } else { if (modelIndex - change.length >= change.modelRowCount) { return -1; } return sorter.convertRowIndexToView( modelIndex - change.length); } } // else, updated } if (modelIndex >= sorter.getModelRowCount()) { return -1; } return sorter.convertRowIndexToView(modelIndex); } private int convertRowIndexToModel(RowSorterEvent e, int viewIndex) { // JW: the event is null if the selection is cached in prepareChange // after model notification. Then the conversion from the // sorter is still valid as the prepare is called before // notifying the sorter. if (e != null) { if (e.getPreviousRowCount() == 0) { return viewIndex; } // range checking handled by RowSorterEvent return e.convertPreviousRowIndexToModel(viewIndex); } // Make sure the viewIndex is valid if (viewIndex < 0 || viewIndex >= sorter.getViewRowCount()) { return -1; } return sorter.convertRowIndexToModel(viewIndex); } /** * Converts the selection to model coordinates. This is used when * the model changes or the sorter changes. */ private int[] convertSelectionToModel(RowSorterEvent e) { int[] selection = list.getSelectedIndices(); for (int i = selection.length - 1; i >= 0; i--) { selection[i] = convertRowIndexToModel(e, selection[i]); } return selection; } //------------------ /** * Notifies the sorter of a change in the underlying model. */ private void notifySorter(ModelChange change) { try { ignoreSortChange = true; sorterChanged = false; if (change.allRowsChanged) { sorter.allRowsChanged(); } else { switch (change.type) { case ListDataEvent.CONTENTS_CHANGED: sorter.rowsUpdated(change.startModelIndex, change.endModelIndex); break; case ListDataEvent.INTERVAL_ADDED: sorter.rowsInserted(change.startModelIndex, change.endModelIndex); break; case ListDataEvent.INTERVAL_REMOVED: sorter.rowsDeleted(change.startModelIndex, change.endModelIndex); break; } } } finally { ignoreSortChange = false; } } private ListSelectionModel getViewSelectionModel() { return list.getSelectionModel(); } /** * Invoked when the underlying model has completely changed. */ private void allChanged() { modelLeadIndex = -1; modelSelection = null; } //------------------- implementing listeners /** * Creates and returns a RowSorterListener. This implementation * calls sortedChanged if the event is of type SORTED. * * @return rowSorterListener to install on sorter. */ protected RowSorterListener createRowSorterListener() { RowSorterListener l = new RowSorterListener() { @Override public void sorterChanged(RowSorterEvent e) { if (e.getType() == RowSorterEvent.Type.SORTED) { sortedChanged(e); } } }; return l; } /** * ModelChange is used when sorting to restore state, it corresponds * to data from a TableModelEvent. The values are precalculated as * they are used extensively.

* * PENDING JW: this is not yet fully adapted to ListDataEvent. */ final static class ModelChange { // JW: if we received a dataChanged, there _is no_ notion // of end/start/length of change // Starting index of the change, in terms of the model, -1 if dataChanged int startModelIndex; // Ending index of the change, in terms of the model, -1 if dataChanged int endModelIndex; // Length of the change (end - start + 1), - 1 if dataChanged int length; // Type of change int type; // Number of rows in the model int modelRowCount; // True if the event indicates all the contents have changed boolean allRowsChanged; public ModelChange(ListDataEvent e) { type = e.getType(); modelRowCount = ((ListModel) e.getSource()).getSize(); startModelIndex = e.getIndex0(); endModelIndex = e.getIndex1(); allRowsChanged = startModelIndex < 0; length = allRowsChanged ? -1 : endModelIndex - startModelIndex + 1; } } }





© 2015 - 2024 Weber Informatics LLC | Privacy Policy