/* This file belongs to the Servoy development and deployment environment, Copyright (C) 1997-2010 Servoy BV This program is free software; you can redistribute it and/or modify it under the terms of the GNU Affero General Public License as published by the Free Software Foundation; either version 3 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 Affero General Public License for more details. You should have received a copy of the GNU Affero General Public License along with this program; if not, see http://www.gnu.org/licenses or write to the Free Software Foundation,Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 */ package com.servoy.j2db.util.model; import java.util.ArrayList; import java.util.Arrays; import java.util.Collections; import java.util.List; import javax.swing.DefaultListSelectionModel; import javax.swing.ListSelectionModel; import javax.swing.event.ListDataEvent; import javax.swing.event.ListDataListener; import com.servoy.j2db.IFormController; import com.servoy.j2db.dataprocessing.EditRecordList; import com.servoy.j2db.dataprocessing.IRecord; import com.servoy.j2db.dataprocessing.ISaveConstants; import com.servoy.j2db.dataprocessing.ISwingFoundSet; import com.servoy.j2db.util.Debug; /** * Selection model that will always keep at least one row selected * * @author gboros */ public class AlwaysRowSelectedSelectionModel extends DefaultListSelectionModel implements ListDataListener { private final List<IFormController> formControllers; private final ISwingFoundSet foundset; private boolean isPrinting = false; // the following two flags I think can be removed altogether if we override insertIndexInterval and removeIndexInterval completely, so super.these are only called // from foundset ListDataListener - this way no UI JTable/JList will decide, only the foundset itself; not completely sure about this though private boolean foundsetIsFiringSizeChange = false; private boolean selectionAlreadyAdjustedBySizeChangeListeners = false; private int rowBeforeSelectionListeners; public AlwaysRowSelectedSelectionModel(ISwingFoundSet foundset) { this.foundset = foundset; formControllers = Collections.synchronizedList(new ArrayList<IFormController>(3)); setSelectionMode(SINGLE_SELECTION); } public void addFormController(IFormController formController) { if (formController != null && !formControllers.contains(formController)) { formControllers.add(formController); } } public void removeFormController(IFormController formController) { if (formController != null) { formControllers.remove(formController); } } /** * used to allow setting the selection to -1 when size >0 for printing */ public void hideSelectionForPrinting() { this.isPrinting = true; this.setSelectedRow(-1); this.isPrinting = false; } private boolean testStopUIEditing() { for (IFormController fco : formControllers.toArray(new IFormController[formControllers.size()])) { if (fco.isFormVisible()) { EditRecordList editRecordList = fco.getApplication().getFoundSetManager().getEditRecordList(); IRecord[] editedRecords = editRecordList.getEditedRecords(fco.getFoundSet()); if (editedRecords.length > 0) { int stopEditing = editRecordList.stopEditing(false, Arrays.asList(editedRecords)); return stopEditing == ISaveConstants.STOPPED || stopEditing == ISaveConstants.AUTO_SAVE_BLOCKED; } } } return true; } @Override public void insertIndexInterval(int index, int length, boolean before) { if (!(foundsetIsFiringSizeChange && selectionAlreadyAdjustedBySizeChangeListeners)) { // in this case the selection was already changed after the actual insert took place (by other foundset listeners) - so calling this method can result in incorrect selection; // so ignore, as a new selection was already decided upon for after this insert selectionAlreadyAdjustedBySizeChangeListeners = true; super.insertIndexInterval(index, length, before); } } @Override public void removeIndexInterval(int index0, int index1) { if (!(foundsetIsFiringSizeChange && selectionAlreadyAdjustedBySizeChangeListeners)) { // in this case the selection was already changed after the actual insert took place (by other foundset listeners) - so calling this method can result in incorrect selection; // so ignore, as a new selection was already decided upon for after this remove selectionAlreadyAdjustedBySizeChangeListeners = true; // if (getSelectionMode() == SINGLE_SELECTION) { int selectedRow = getSelectedRow();//save the selection this.rowBeforeSelectionListeners = Integer.MIN_VALUE;//make sure we start fresh if (selectedRow >= index0 && selectedRow <= index1 && foundset.getSize() > 0) { // selected record was removed, set selection after the removed block or before (if at the end) // note: default behavior of DefaultListSelectionModel is to set selected index to -1 when selected was removed int selection = Math.min(index0, foundset.getSize()); // if it is set to the foundset.getSize() - 1 but the foundset had more rows, then just select the first.. // else it will load in the next pks and the selection will be somewhere in the middle if (selection == foundset.getSize() && foundset.hadMoreRows()) { selection = 0; //set the selection to the first row first super.setSelectionInterval(selection, selection); //and then remove the interval because this way the selection does not remain on the last row //and it won't fetch the next rows super.removeIndexInterval(index0, index1); } else { super.removeIndexInterval(index0, index1);//this can trigger selectionListeners that may include executing onRecordSelect, which can change the selection int rowAfterSelectionListeners = getSelectedRow();//get the selected row after the new row is selected //adjust the selection if super.removeIndexInterval() did not set it correctly but only if the listeners (onRecordSelect) didn't already adjust it if (selection != rowAfterSelectionListeners && (rowBeforeSelectionListeners != Integer.MIN_VALUE && rowAfterSelectionListeners == rowBeforeSelectionListeners) && selection < foundset.getSize()) { // i have to call the setSelectionInterval else our methods will test if the record is there super.setSelectionInterval(selection, selection); } } } else { super.removeIndexInterval(index0, index1); } } // else // { // super.removeIndexInterval(index0, index1); // } } } public boolean setSelectedRow(int row) { if (getSelectionMode() == SINGLE_SELECTION && row == getSelectedRow()) return true; if (!canChangeSelection()) return false; return setSelectedRow(row, false, true); } public boolean canChangeSelection() { int currentSelectedRow = getSelectedRow(); if (currentSelectedRow != -1 && !testStopUIEditing()) { if (Debug.tracing()) { Debug.trace("could not leave record, validation or save failed"); //$NON-NLS-1$ } return false; } return true; } private boolean setSelectedRow(int row, boolean keepOldSelections, boolean valueIsAdjusting) { if (row >= 0 && !getRecordAndTestSize(row)) { return false; } boolean oldIsAdjusting = getValueIsAdjusting(); setValueIsAdjusting(valueIsAdjusting); if (row == -1) { if (foundsetIsFiringSizeChange) selectionAlreadyAdjustedBySizeChangeListeners = true; clearSelection(); } else setSelectedRows(new int[] { row }, keepOldSelections); setValueIsAdjusting(oldIsAdjusting); return true; } public int getSelectedRow() { int[] selectedRows = getSelectedRows(); return selectedRows.length > 0 ? selectedRows[0] : -1; } public void setSelectedRows(int[] rows) { setSelectedRows(rows, false); } private void setSelectedRows(int[] rows, boolean keepOldSelections) { if (rows != null && rows.length > 0) { for (int row : rows) { if (row == -1) continue; IRecord record = foundset.getRecord(row); if (record == null || record == foundset.getPrototypeState()) { // don't allow this selection at all return; } } if (foundsetIsFiringSizeChange) selectionAlreadyAdjustedBySizeChangeListeners = true; if (keepOldSelections) addSelectionInterval(rows[0], rows[0]); else setSelectionInterval(rows[0], rows[0]); for (int i = 1; i < rows.length; i++) addSelectionInterval(rows[i], rows[i]); fireValueChanged(false); } } @Override protected void fireValueChanged(int firstIndex, int lastIndex, boolean isAdjusting) { // make sure if the selection is still -1 but the size > 0 (and the SelectionModel wanted to fire because of the index added index changes) // that we just set the selected row to the first index. (and that will do the real fire) if (getSelectedRow() == -1 && (firstIndex != lastIndex || !isPrinting) && foundset.getSize() > 0) { //save the selected row for removeIndexInterval this.rowBeforeSelectionListeners = firstIndex; setSelectedRow(firstIndex); } else { super.fireValueChanged(firstIndex, lastIndex, isAdjusting); } } public int[] getSelectedRows() { ArrayList<Integer> selectedIndexes = new ArrayList<Integer>(); int minIdx = getMinSelectionIndex(); int maxIdx = getMaxSelectionIndex(); for (int i = minIdx; i <= maxIdx; i++) { if (isSelectedIndex(i)) selectedIndexes.add(new Integer(i)); } int[] iSelectedIndexes = new int[selectedIndexes.size()]; for (int i = 0; i < selectedIndexes.size(); i++) iSelectedIndexes[i] = selectedIndexes.get(i).intValue(); return iSelectedIndexes; } public void fireAdjusting() { fireValueChanged(true); } public boolean setFoundsetIsFiringSizeChangeTableAndListEvent(boolean b) { boolean before = foundsetIsFiringSizeChange; foundsetIsFiringSizeChange = b; selectionAlreadyAdjustedBySizeChangeListeners = false; return before; } public void contentsChanged(ListDataEvent e) { // nothing useful here } public void intervalAdded(ListDataEvent e) { insertIndexInterval(e.getIndex0(), e.getIndex1() - e.getIndex0() + 1, true); } public void intervalRemoved(ListDataEvent e) { removeIndexInterval(e.getIndex0(), e.getIndex1()); } @Override public void setSelectionMode(int selectionMode) { int oldSelection = getSelectionMode(); super.setSelectionMode(selectionMode); if (selectionMode == ListSelectionModel.SINGLE_SELECTION && selectionMode != oldSelection) { setSelectedRow(getSelectedRow(), false, true); } } @Override public void setSelectionInterval(int index0, int index1) { if (index1 > index0) { if (!getRecordAndTestSize(index1)) return; } else if (!getRecordAndTestSize(index0)) { return; } super.setSelectionInterval(index0, index1); } @Override public void setAnchorSelectionIndex(int anchorIndex) { if (!getRecordAndTestSize(anchorIndex)) return; super.setAnchorSelectionIndex(anchorIndex); } /** * @param index */ private boolean getRecordAndTestSize(int index) { foundset.getRecord(index); // don't allow selection beyond the size if (foundset.getSize() <= index) return false; return true; } }