/******************************************************************************* * Breakout Cave Survey Visualizer * * Copyright (C) 2014 James Edwards * * jedwards8 at fastmail dot fm * * This program is free software; you can redistribute it and/or modify it under * the terms of the GNU General Public License as published by the Free Software * Foundation; either version 2 of the License, or (at your option) any later * version. * * This program 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 General Public License for more * details. * * You should have received a copy of the GNU General Public License along with * this program; if not, write to the Free Software Foundation, Inc., 51 * Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. *******************************************************************************/ package org.andork.swing.table; import java.util.Vector; import javax.swing.DefaultListSelectionModel; import javax.swing.JTable; import javax.swing.ListSelectionModel; import javax.swing.RowSorter; import javax.swing.event.ListSelectionEvent; import javax.swing.event.RowSorterEvent; import javax.swing.event.RowSorterEvent.Type; import javax.swing.event.TableModelEvent; import javax.swing.table.TableColumnModel; import javax.swing.table.TableModel; import org.andork.swing.AnnotatingRowSorter; /** * A {@link JTable} with the following added features: * <ul> * <li>It does a perfect job of maintaining the row selections when row * visibility/order changes. * <li>You can {@linkplain #getModelSelectionModel() get} and * {@linkplain #setModelSelectionModel(ListSelectionModel) set} the model * selection model; {@code BetterJTable} will update the model selection model * when the {@linkplain #getSelectionModel() view selection model} changes, and * vice versa. * </ul> * * * @author Andy */ @SuppressWarnings("serial") public class BetterJTable extends JTable { /** * */ private static final long serialVersionUID = 7877994145179355425L; protected ListSelectionModel modelSelectionModel = null; protected boolean ignoreModelSelectionChanges = false; protected boolean ignoreViewSelectionChanges = false; public BetterJTable() { super(); init(); } public BetterJTable(int numRows, int numColumns) { super(numRows, numColumns); init(); } public BetterJTable(Object[][] rowData, Object[] columnNames) { super(rowData, columnNames); init(); } public BetterJTable(TableModel dm) { super(dm); init(); } public BetterJTable(TableModel dm, TableColumnModel cm) { super(dm, cm); init(); } public BetterJTable(TableModel dm, TableColumnModel cm, ListSelectionModel sm) { super(dm, cm, sm); init(); } public BetterJTable(Vector rowData, Vector columnNames) { super(rowData, columnNames); init(); } /** * @return the {@link ListSelectionModel} of selected model indices. * {@code BetterJTable} will automatically keep it in sync with the * view selection model. */ public ListSelectionModel getModelSelectionModel() { return modelSelectionModel; } protected void init() { setModelSelectionModel(new DefaultListSelectionModel()); } protected void rebuildModelSelectionModel() { ListSelectionModel viewSelectionModel = getSelectionModel(); if (viewSelectionModel == null || modelSelectionModel == null) { return; } ignoreModelSelectionChanges = true; try { try { modelSelectionModel.setValueIsAdjusting(true); if (modelSelectionModel.getMinSelectionIndex() >= 0) { modelSelectionModel.removeSelectionInterval(modelSelectionModel.getMinSelectionIndex(), modelSelectionModel.getMaxSelectionIndex()); } int index0 = -1; int viewIndex, modelIndex; for (modelIndex = 0; modelIndex < getRowCount(); modelIndex++) { viewIndex = convertRowIndexToView(modelIndex); if (viewSelectionModel.isSelectedIndex(viewIndex)) { if (index0 < 0) { index0 = modelIndex; } } else if (index0 >= 0) { modelSelectionModel.addSelectionInterval(index0, modelIndex - 1); index0 = -1; } } if (index0 >= 0) { modelSelectionModel.addSelectionInterval(index0, modelIndex - 1); } } finally { modelSelectionModel.setValueIsAdjusting(viewSelectionModel.getValueIsAdjusting()); } } finally { ignoreModelSelectionChanges = false; } } protected void rebuildViewSelectionModel() { ignoreViewSelectionChanges = true; try { ListSelectionModel viewSelectionModel = getSelectionModel(); if (viewSelectionModel == null || modelSelectionModel == null) { return; } try { viewSelectionModel.setValueIsAdjusting(true); if (viewSelectionModel.getMinSelectionIndex() >= 0) { viewSelectionModel.removeSelectionInterval(viewSelectionModel.getMinSelectionIndex(), viewSelectionModel.getMaxSelectionIndex()); } int index0 = -1; int modelIndex, viewIndex; for (viewIndex = 0; viewIndex < getRowCount(); viewIndex++) { modelIndex = convertRowIndexToModel(viewIndex); if (modelSelectionModel.isSelectedIndex(modelIndex)) { if (index0 < 0) { index0 = viewIndex; } } else if (index0 >= 0) { viewSelectionModel.addSelectionInterval(index0, viewIndex - 1); index0 = -1; } } if (index0 >= 0) { viewSelectionModel.addSelectionInterval(index0, viewIndex - 1); } } finally { viewSelectionModel.setValueIsAdjusting(modelSelectionModel.getValueIsAdjusting()); } } finally { ignoreViewSelectionChanges = false; } } @Override public void setModel(TableModel dataModel) { if (dataModel != getModel()) { ignoreViewSelectionChanges = true; try { super.setModel(dataModel); } finally { ignoreViewSelectionChanges = false; } rebuildViewSelectionModel(); } } /** * Sets the {@link ListSelectionModel} of selected model indices. The view * selection model will be immediately updated to match the new model * selection model. * * @return the {@link ListSelectionModel} of selected model indices. * {@code BetterJTable} will automatically keep it in sync with the * view selection model. */ public void setModelSelectionModel(ListSelectionModel newModel) { if (modelSelectionModel != newModel) { if (modelSelectionModel != null) { modelSelectionModel.removeListSelectionListener(this); } modelSelectionModel = newModel; rebuildViewSelectionModel(); if (newModel != null) { newModel.addListSelectionListener(this); } } } @Override public void setRowSorter(RowSorter<? extends TableModel> sorter) { if (sorter != getRowSorter()) { ignoreViewSelectionChanges = true; try { super.setRowSorter(sorter); } finally { ignoreViewSelectionChanges = false; } rebuildViewSelectionModel(); } } @Override public void setSelectionModel(ListSelectionModel newModel) { if (newModel != getSelectionModel()) { ignoreViewSelectionChanges = true; try { super.setSelectionModel(newModel); rebuildModelSelectionModel(); } finally { ignoreViewSelectionChanges = false; } } } @Override public void sorterChanged(RowSorterEvent e) { ignoreViewSelectionChanges = true; try { super.sorterChanged(e); } finally { ignoreViewSelectionChanges = false; } updateViewSelectionModel(e); } @Override public void tableChanged(TableModelEvent e) { ignoreViewSelectionChanges = true; try { super.tableChanged(e); updateModelSelectionModel(e); } finally { ignoreViewSelectionChanges = false; } } protected void updateModelSelectionModel(ListSelectionEvent e) { if (modelSelectionModel == null) { return; } ignoreModelSelectionChanges = true; try { try { modelSelectionModel.setValueIsAdjusting(true); ListSelectionModel selectionModel = (ListSelectionModel) e.getSource(); for (int viewIndex = Math.max(0, e.getFirstIndex()); viewIndex <= Math.min(getRowCount() - 1, e.getLastIndex()); viewIndex++) { int modelIndex = convertRowIndexToModel(viewIndex); if (selectionModel.isSelectedIndex(viewIndex)) { modelSelectionModel.addSelectionInterval(modelIndex, modelIndex); } else { modelSelectionModel.removeSelectionInterval(modelIndex, modelIndex); } } } finally { modelSelectionModel.setValueIsAdjusting(e.getValueIsAdjusting()); } } finally { ignoreModelSelectionChanges = false; } } protected void updateModelSelectionModel(TableModelEvent e) { if (modelSelectionModel == null) { return; } switch (e.getType()) { case TableModelEvent.UPDATE: if (e.getFirstRow() == 0 && e.getLastRow() == Integer.MAX_VALUE && e.getColumn() == TableModelEvent.ALL_COLUMNS) { modelSelectionModel.clearSelection(); } break; case TableModelEvent.INSERT: modelSelectionModel.insertIndexInterval(e.getFirstRow(), e.getLastRow(), true); break; case TableModelEvent.DELETE: modelSelectionModel.removeIndexInterval(e.getFirstRow(), e.getLastRow()); break; } } protected void updateViewSelectionModel(ListSelectionEvent e) { ListSelectionModel viewSelectionModel = getSelectionModel(); if (viewSelectionModel == null) { return; } ignoreViewSelectionChanges = true; try { try { viewSelectionModel.setValueIsAdjusting(true); ListSelectionModel selectionModel = (ListSelectionModel) e.getSource(); for (int modelIndex = Math.max(0, e.getFirstIndex()); modelIndex <= Math .min(getModel().getRowCount() - 1, e.getLastIndex()); modelIndex++) { int viewIndex = convertRowIndexToView(modelIndex); if (selectionModel.isSelectedIndex(modelIndex)) { viewSelectionModel.addSelectionInterval(viewIndex, viewIndex); } else { viewSelectionModel.removeSelectionInterval(viewIndex, viewIndex); } } } finally { viewSelectionModel.setValueIsAdjusting(e.getValueIsAdjusting()); } } finally { ignoreViewSelectionChanges = false; } } protected void updateViewSelectionModel(RowSorterEvent e) { if (e.getType() == Type.SORTED) { rebuildViewSelectionModel(); } } @Override public void valueChanged(ListSelectionEvent e) { if (e.getSource() == getSelectionModel()) { super.valueChanged(e); if (!e.getValueIsAdjusting() && !ignoreViewSelectionChanges) { updateModelSelectionModel(e); } } else if (e.getSource() == modelSelectionModel) { if (!e.getValueIsAdjusting() && !ignoreModelSelectionChanges) { if (getRowSorter() instanceof AnnotatingRowSorter && ((AnnotatingRowSorter) getRowSorter()).isSortingInBackground()) { return; } updateViewSelectionModel(e); } } } }