/*
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.editlist;
import java.applet.Applet;
import java.awt.Component;
import java.awt.Point;
import java.awt.Rectangle;
import java.awt.Window;
import java.awt.event.ActionEvent;
import java.awt.event.InputEvent;
import java.awt.event.KeyEvent;
import java.awt.event.MouseEvent;
import java.beans.PropertyChangeEvent;
import java.beans.PropertyChangeListener;
import java.util.EventObject;
import javax.swing.AbstractAction;
import javax.swing.Action;
import javax.swing.ActionMap;
import javax.swing.DefaultCellEditor;
import javax.swing.FocusManager;
import javax.swing.InputMap;
import javax.swing.JButton;
import javax.swing.JComponent;
import javax.swing.JLabel;
import javax.swing.JList;
import javax.swing.KeyStroke;
import javax.swing.ListCellRenderer;
import javax.swing.ListModel;
import javax.swing.ListSelectionModel;
import javax.swing.SwingUtilities;
import javax.swing.event.CellEditorListener;
import javax.swing.event.ChangeEvent;
import javax.swing.event.ListDataEvent;
import javax.swing.event.ListDataListener;
import javax.swing.event.ListSelectionEvent;
import javax.swing.event.ListSelectionListener;
import javax.swing.plaf.ComponentUI;
import javax.swing.text.Position.Bias;
import com.servoy.j2db.util.HtmlUtils;
import com.servoy.j2db.util.ISkinnable;
import com.servoy.j2db.util.Utils;
import com.servoy.j2db.util.model.AlwaysRowSelectedSelectionModel;
import com.servoy.j2db.util.model.IEditListModel;
//"editable" JList
public class JEditList extends JList implements CellEditorListener, ISkinnable, ListSelectionListener
{
private IEditListEditor cellEditor = null;
private Component editorComp = null;
private CellEditorRemover editorRemover = null;
private int editingRow = -1;
private boolean isRendererSameAsEditor = false;
private boolean surrendersFocusOnKeystroke = false;
private final EmptyModelDataListener myListDataListener;
private boolean editable = true;
private boolean mustSelectFirst = false;
private JLabel labelNoResults;
public JEditList()
{
super();
setUI(new EditListUI());
// super.setSelectionMode(ListSelectionModel.SINGLE_SELECTION);
// fix for case 188224; if you had a record view form and a list view form both visible in the same
// window, and a text field with no onAction registered from the record view has focus, pressing ENTER
// would result in focus being moved to the list view; we must avoid this by using WHEN_FOCUSED
getInputMap(WHEN_FOCUSED).put(KeyStroke.getKeyStroke(KeyEvent.VK_ENTER, 0), "enter"); //$NON-NLS-1$
getInputMap(WHEN_FOCUSED).put(KeyStroke.getKeyStroke(KeyEvent.VK_TAB, 0), "enter"); //$NON-NLS-1$
getActionMap().put("enter", new AbstractAction() //$NON-NLS-1$
{
public void actionPerformed(ActionEvent event)
{
if (!isEditing())
{
editCellAt(getSelectedIndex(), event);
if (editorComp != null) editorComp.transferFocus();
}
}
});
myListDataListener = new EmptyModelDataListener();
setModel(new AbstractEditListModel()
{
public Object getElementAt(int i)
{
return null;
}
public int getSize()
{
return 0;
}
});
//removed copy (CTRL+C) action from JList
InputMap im = this.getInputMap(JComponent.WHEN_FOCUSED);
ActionMap am = this.getActionMap();
Action copyAction = new AbstractAction()
{
public void actionPerformed(ActionEvent e)
{
//do nothing
}
};
im.put(KeyStroke.getKeyStroke(KeyEvent.VK_C, InputEvent.CTRL_MASK), "Copy");
am.put("Copy", copyAction);
}
protected int checkModelSize()
{
int sz = getModel().getSize();
// if(sz == 0)
// {
// if(labelNoResults == null)
// {
// labelNoResults = new JLabel("No results");
// labelNoResults.setBounds(10, 10,300,25);
// add(labelNoResults);
// }
// }
// else if(labelNoResults != null)
// {
// remove(labelNoResults);
// labelNoResults = null;
// }
return sz;
}
@Override
public void setToolTipText(String tip)
{
if (!Utils.stringIsEmpty(tip))
{
if (!Utils.stringContainsIgnoreCase(tip, "<html")) //$NON-NLS-1$
{
super.setToolTipText(tip);
}
else if (HtmlUtils.hasUsefulHtmlContent(tip))
{
super.setToolTipText(tip);
}
}
else
{
super.setToolTipText(null);
}
}
public void setUI(EditListUI ui)
{
if (ui instanceof EditListUI)
{
super.setUI(ui);
}
}
@Override
public void setUI(ComponentUI ui)
{
// super.setUI(ui); do nothing has his own UI
}
// public void setSelectionMode(int i)
// {
// //ignore
// }
/**
* @return Returns the editable.
*/
public boolean isEditable()
{
return editable;
}
/**
* @param editable The editable to set.
*/
public void setEditable(boolean editable)
{
this.editable = editable;
if (!editable && isEditing())
{
getCellEditor().stopCellEditing();
}
setEnabled(editable);
}
/**
* Sets whether editors in this JEditList get the keyboard focus when an editor is activated as a result of the JEditList forwarding keyboard events for a
* cell. By default, this property is false, and the JEditList retains the focus unless the cell is clicked.
*
* @param surrendersFocusOnKeystroke true if the editor should get the focus when keystrokes cause the editor to be activated
*
*
* @see #getSurrendersFocusOnKeystroke
*/
public void setSurrendersFocusOnKeystroke(boolean surrendersFocusOnKeystroke)
{
this.surrendersFocusOnKeystroke = surrendersFocusOnKeystroke;
}
/**
* Returns true if the editor should get the focus when keystrokes cause the editor to be activated
*
* @return true if the editor should get the focus when keystrokes cause the editor to be activated
*
* @see #setSurrendersFocusOnKeystroke
*/
public boolean getSurrendersFocusOnKeystroke()
{
return surrendersFocusOnKeystroke;
}
/**
* Programmatically starts editing the cell at <code>row</code>
*
* @param row the row to be edited
* @exception IllegalArgumentException if <code>row</code> is not in the valid range
* @return false if for any reason the cell cannot be edited
*/
public boolean editCellAt(int row)
{
return editCellAt(row, null);
}
/**
* Programmatically starts editing the cell at <code>row</code> , if the cell is editable. To prevent the <code>JEditList</code> from editing a particular
* table, column or cell value, return false from the <code>isCellEditable</code> method in the <code>TableModel</code> interface.
*
* @param row the row to be edited
* @param e event to pass into <code>shouldSelectCell</code>; note that as of Java 2 platform v1.2, the call to <code>shouldSelectCell</code> is no longer
* made
* @exception IllegalArgumentException if <code>row</code> is not in the valid range
* @return false if for any reason the cell cannot be edited
*/
public boolean editCellAt(int row, EventObject e)
{
if (getEditingRow() == row) return true;
//System.out.println("editCellAt0");
if (cellEditor != null && !cellEditor.stopCellEditing())
{
return false;
}
//System.out.println("editCellAt1");
if (row < 0 || row >= getModel().getSize())
{
return false;
}
FocusManager fm = FocusManager.getCurrentManager();
//System.out.println("editCellAt3");
if (editorRemover == null)
{
editorRemover = new CellEditorRemover(fm);
//DISABLED:SHOULDFIX:fm.addPropertyChangeListener("focusOwner", editorRemover);
}
//System.out.println("IEditListEditor isCellEditable: "+e);
IEditListEditor editor = getCellEditor();
if (editor != null && editor.isCellEditable(e))
{
//System.out.println(" preparing editor ");
editorComp = prepareEditor(editor, row);
if (editorComp == null)
{
removeEditor();
return false;
}
//System.out.println("editorComp "+editorComp);
editorComp.setBounds(getCellBounds(row, row));
add(editorComp);
editorComp.validate();
// setCellEditor(editor);
setEditingRow(row);
editor.addCellEditorListener(this);
//fm.focusNextComponent(editorComp);
return true;
}
return false;
}
protected boolean shouldDispatchClickToButton(JButton button)
{
return true;
}
/**
* Returns true if a cell is being edited.
*
* @return true if the table is editing a cell
* @see #editingRow
*/
public boolean isEditing()
{
return editorComp != null;
}
/**
* Returns the component that is handling the editing session. If nothing is being edited, returns null.
*
* @return Component handling editing session
*/
public Component getEditorComponent()
{
return editorComp;
}
/**
* Returns the index of the row that contains the cell currently being edited. If nothing is being edited, returns -1.
*
* @return the index of the row that contains the cell currently being edited; returns -1 if nothing being edited
*/
public int getEditingRow()
{
return editingRow;
}
/*
* protected boolean processKeyBinding(KeyStroke ks, KeyEvent e,int condition, boolean pressed) { //System.out.println("processKeyBinding"); boolean
* retValue = super.processKeyBinding(ks, e, condition, pressed); // Start editing when a key is typed. UI classes can disable this behavior // by setting
* the client property JEditList.autoStartsEdit to Boolean.FALSE. if (!retValue && condition == WHEN_ANCESTOR_OF_FOCUSED_COMPONENT && isFocusOwner() //&& //
* !Boolean.FALSE.equals((Boolean)getClientProperty("JEditList.autoStartsEdit")) ) { // We do not have a binding for the event. Component editorComponent =
* getEditorComponent(); //System.out.println("invoke getEditorComponent "+editorComponent); if (editorComponent == null) { //System.out.println("e "+e); //
* Only attempt to install the editor on a KEY_PRESSED, if (e == null || e.getID() != KeyEvent.KEY_PRESSED) { return false; } // Don't start when just a
* modifier is pressed int code = e.getKeyCode(); //System.out.println("code "+code); if (code == KeyEvent.VK_SHIFT || code == KeyEvent.VK_CONTROL || code
* == KeyEvent.VK_ALT) { return false; } // Try to install the editor int anchorRow = getSelectionModel().getAnchorSelectionIndex();
* //System.out.println("invoke getAnchorSelectionIndex"); if (anchorRow != -1 && !isEditing()) { //System.out.println("invoke editCellAt"); if
* (!editCellAt(anchorRow)) { return false; } } editorComponent = getEditorComponent(); if (editorComponent == null) { return false; } } // If the
* editorComponent is a JComponent, pass the event to it. if (editorComponent instanceof JComponent) { retValue =
* ((JComponent)editorComponent).processKeyBinding(ks, e, WHEN_FOCUSED, pressed); // If we have started an editor as a result of the user // pressing a key
* and the surrendersFocusOnKeystroke property // is true, give the focus to the new editor. if (getSurrendersFocusOnKeystroke()) {
* editorComponent.requestFocus(); } } }
*
* return retValue; }
*/
//
// Implementing the CellEditorListener interface
//
/**
* Invoked when editing is finished. The changes are saved and the editor is discarded.
* <p>
* Application code will not use these methods explicitly, they are used internally by JEditList.
*
* @param e the event received
* @see CellEditorListener
*/
public void editingStopped(ChangeEvent e)
{
// Take in the new value
IEditListEditor editor = getCellEditor();
if (editor != null)
{
Object value = editor.getCellEditorValue();
((IEditListModel)getModel()).setElementAt(value, editingRow);
removeEditor();
}
}
/**
* Invoked when editing is canceled. The editor object is discarded and the cell is rendered once again.
* <p>
* Application code will not use these methods explicitly, they are used internally by JEditList.
*
* @param e the event received
* @see CellEditorListener
*/
public void editingCanceled(ChangeEvent e)
{
removeEditor();
}
/**
* Sets the <code>cellEditor</code> variable.
*
* @param anEditor the IEditListEditor that does the editing
* @see #cellEditor
* @beaninfo bound: true description: The table's active cell editor, if one exists.
*/
public void setCellEditor(IEditListEditor anEditor)
{
IEditListEditor oldEditor = cellEditor;
cellEditor = anEditor;
firePropertyChange("listViewEditor", oldEditor, anEditor); //$NON-NLS-1$
}
/**
* Returns an appropriate editor for the cell specified by <code>row</code>.
* <p>
* <b>Note:</b> Throughout the table package, the internal implementations always use this method to provide editors so that this default behavior can be
* safely overridden by a subclass.
*
* @param row the row of the cell to edit, where 0 is the first row
* @return the editor for this cell; if <code>null</code> return the default editor for this type of cell
* @see DefaultCellEditor
*/
public IEditListEditor getCellEditor()
{
return cellEditor;
}
/**
* Sets the <code>editingRow</code> variable.
*
* @param aRow the row of the cell to be edited
*
* @see #editingRow
*/
public void setEditingRow(int aRow)
{
editingRow = aRow;
}
/**
* Prepares the editor by querying the data model for the value and selection state of the cell at <code>row</code>.
* <p>
* <b>Note:</b> Throughout the table package, the internal implementations always use this method to prepare editors so that this default behavior can be
* safely overridden by a subclass.
*
* @param editor the <code>IEditListEditor</code> to set up
* @param row the row of the cell to edit, where 0 is the first row
* @return the <code>Component</code> being edited
*/
public Component prepareEditor(IEditListEditor editor, int row)
{
Object value = getModel().getElementAt(row);
Component comp = editor.getListCellEditorComponent(this, value, true, row);
if (comp instanceof JComponent)
{
JComponent jComp = (JComponent)comp;
if (jComp.getNextFocusableComponent() == null)
{
jComp.setNextFocusableComponent(this);
}
}
return comp;
}
/**
* Discards the editor object and frees the real estate it used for cell rendering.
*/
public void removeEditor()
{
//DISABLED:SHOULDFIX:DefaultFocusManager.getCurrentManager().removePropertyChangeListener("focusOwner",editorRemover);
IEditListEditor editor = getCellEditor();
if (editor != null)
{
editor.removeCellEditorListener(this);
if (editorComp != null)
{
remove(editorComp);
Rectangle cellRect = getCellBounds(editingRow, editingRow);
if (cellRect != null) repaint(cellRect);
setEditingRow(-1);
editorComp = null;
}
requestFocus();
}
}
// This class tracks changes in the keyboard focus state. It is used
// when the JEditList is editing to determine when to cancel the edit.
// If focus switches to a component outside of the JEditList, but in the
// same window, this will cancel editing.
class CellEditorRemover implements PropertyChangeListener
{
FocusManager focusManager;
public CellEditorRemover(FocusManager fm)
{
this.focusManager = fm;
}
public void propertyChange(PropertyChangeEvent ev)
{
if (!isEditing())
{
return;
}
Component c = null;//DISABLED:SHOULDFIX:focusManager.getFocusOwner();
while (c != null)
{
if (c == JEditList.this)
{
// focus remains inside the table
return;
}
else if ((c instanceof Window) || (c instanceof Applet && c.getParent() == null))
{
if (c == SwingUtilities.getRoot(JEditList.this))
{
getCellEditor().cancelCellEditing();
}
break;
}
c = c.getParent();
}
}
}
/*
* (non-Javadoc)
*
* @see javax.swing.JList#setSelectionModel(javax.swing.ListSelectionModel)
*/
@Override
public void setSelectionModel(ListSelectionModel selectionModel)
{
ListSelectionModel lsm = getSelectionModel();
if (lsm != null) lsm.removeListSelectionListener(this);
super.setSelectionModel(selectionModel);
selectionModel.addListSelectionListener(this);
}
@Override
public void setModel(ListModel model)
{
if (model == null) return;
// TODO must check if listModel instanceof IEditList??
getModel().removeListDataListener(myListDataListener);
super.setModel(model);
checkModelSize();
getModel().addListDataListener(myListDataListener);
}
class EmptyModelDataListener implements ListDataListener
{
public void contentsChanged(ListDataEvent event)
{
checkModelSize();
}
public void intervalAdded(ListDataEvent event)
{
checkModelSize();
// int sz = checkModelSize();
// if ((getSelectedIndex() == -1 || getSelectedIndex() >= sz) && event.getIndex1() >= 0)
// if (getSelectedIndex() == -1 && event.getIndex1() >= 0)
// {
// setSelectedIndex(0);
// }
//Debug.trace("index "+getSelectedIndex());
}
public void intervalRemoved(ListDataEvent event)
{
if (isEditing() && (getModel().getSize() == 0 || (getEditingRow() >= event.getIndex0() && getEditingRow() <= event.getIndex1())))
{
getCellEditor().stopCellEditing();
}
checkModelSize();
}
}
/*
* @see JList#getNextMatch(String, int, Bias)
*/
@Override
public int getNextMatch(String arg0, int arg1, Bias arg2)
{
// do nothing
return -1;
}
/*
* @see JComponent#getToolTipText(MouseEvent)
*/
@Override
public String getToolTipText(MouseEvent event)
{
if (event != null)
{
Point p = event.getPoint();
int index = locationToIndex(p);
ListCellRenderer r = getCellRenderer();
Rectangle cellBounds;
if (index != -1 && r != null && (cellBounds = getCellBounds(index, index)) != null && cellBounds.contains(p.x, p.y))
{
ListSelectionModel lsm = getSelectionModel();
Component rComponent = r.getListCellRendererComponent(this, getModel().getElementAt(index), index, lsm.isSelectedIndex(index),
(hasFocus() && (lsm.getLeadSelectionIndex() == index)));
if (rComponent instanceof JComponent)
{
MouseEvent newEvent;
p.translate(-cellBounds.x, -cellBounds.y);
newEvent = new MouseEvent(rComponent, event.getID(), event.getWhen(), event.getModifiers(), p.x, p.y, event.getClickCount(),
event.isPopupTrigger());
this.add(rComponent);
rComponent.setBounds(cellBounds);
rComponent.doLayout();
String tip = ((JComponent)rComponent).getToolTipText(newEvent);
this.remove(rComponent);
if (tip != null)
{
return tip;
}
}
}
}
return super.getToolTipText();
}
/**
* Gets the mustSelectFirst. (first the row must be selected before it starts editing)
*
* @return Returns a boolean
*/
public boolean getMustSelectFirst()
{
return mustSelectFirst;
}
/**
* Sets the mustSelectFirst.
*
* @param mustSelectFirst The mustSelectFirst to set
*/
public void setMustSelectFirst(boolean mustSelectFirst)
{
this.mustSelectFirst = mustSelectFirst;
}
/**
* @see javax.swing.JList#setSelectedIndex(int)
*/
@Override
public void setSelectedIndex(int index)
{
if (isEditing())
{
getCellEditor().stopCellEditing();
}
if (getSelectionModel() instanceof AlwaysRowSelectedSelectionModel && !((AlwaysRowSelectedSelectionModel)getSelectionModel()).canChangeSelection()) return;
super.setSelectedIndex(index);
}
/**
* Returns the isRendererSameAsEditor.
*
* @return boolean
*/
public boolean isRendererSameAsEditor()
{
return isRendererSameAsEditor;
}
/**
* Sets the isRendererSameAsEditor.
*
* @param isRendererSameAsEditor The isRendererSameAsEditor to set
*/
public void setRendererSameAsEditor(boolean isRendererSameAsEditor)
{
this.isRendererSameAsEditor = isRendererSameAsEditor;
}
/*
* (non-Javadoc)
*
* @see javax.swing.event.ListSelectionListener#valueChanged(javax.swing.event.ListSelectionEvent)
*/
public void valueChanged(ListSelectionEvent e)
{
if (isEditing())
{
getCellEditor().stopCellEditing();
}
if (getAutoscrolls() && !e.getValueIsAdjusting())
{
int selected = getSelectedIndex();
ensureIndexIsVisible(selected);
}
}
}