001package jmri.swing; 002 003import java.awt.Component; 004import java.awt.KeyboardFocusManager; 005import java.awt.Point; 006import java.awt.Rectangle; 007import java.awt.Window; 008import java.awt.event.ActionEvent; 009import java.awt.event.KeyEvent; 010import java.awt.event.MouseAdapter; 011import java.awt.event.MouseEvent; 012import java.beans.PropertyChangeEvent; 013import java.beans.PropertyChangeListener; 014import java.util.EventObject; 015import javax.swing.AbstractAction; 016import javax.swing.JComponent; 017import javax.swing.JList; 018import javax.swing.KeyStroke; 019import javax.swing.ListModel; 020import javax.swing.ListSelectionModel; 021import javax.swing.SwingUtilities; 022import javax.swing.event.CellEditorListener; 023import javax.swing.event.ChangeEvent; 024 025/** 026 * 027 * @author Randall Wood 028 */ 029public class EditableList<E> extends JList<E> implements CellEditorListener { 030 031 protected Component editorComp = null; 032 protected int editingIndex = -1; 033 protected transient ListCellEditor<E> cellEditor = null; 034 private PropertyChangeListener editorRemover = null; 035 036 public EditableList() { 037 super(new DefaultEditableListModel<>()); 038 init(); 039 } 040 041 public EditableList(ListModel<E> dataModel) { 042 super(dataModel); 043 init(); 044 } 045 046 private void init() { 047 getActionMap().put("startEditing", new StartEditingAction()); // NOI18N 048 getActionMap().put("cancel", new CancelEditingAction()); // NOI18N 049 addMouseListener(new MouseListener()); 050 getInputMap().put(KeyStroke.getKeyStroke(KeyEvent.VK_F2, 0), "startEditing"); // NOI18N 051 getInputMap(JComponent.WHEN_ANCESTOR_OF_FOCUSED_COMPONENT).put(KeyStroke.getKeyStroke(KeyEvent.VK_ESCAPE, 0), "cancel"); // NOI18N 052 putClientProperty("terminateEditOnFocusLost", Boolean.TRUE); // NOI18N 053 } 054 055 public void setListCellEditor(ListCellEditor<E> editor) { 056 this.cellEditor = editor; 057 } 058 059 public ListCellEditor<E> getListCellEditor() { 060 return cellEditor; 061 } 062 063 public boolean isEditing() { 064 return (editorComp != null); 065 } 066 067 public Component getEditorComponent() { 068 return editorComp; 069 } 070 071 public int getEditingIndex() { 072 return editingIndex; 073 } 074 075 @SuppressWarnings( "deprecation" ) // {@link JComponent#setNextFocusableComponent} {@link JComponent#getNextFocusableComponent} 076 public Component prepareEditor(int index) { 077 E value = getModel().getElementAt(index); 078 boolean isSelected = isSelectedIndex(index); 079 Component comp = cellEditor.getListCellEditorComponent(this, value, isSelected, index); 080 if (comp instanceof JComponent) { 081 JComponent jComp = (JComponent) comp; 082 if (jComp.getNextFocusableComponent() == null) { 083 jComp.setNextFocusableComponent(this); 084 } 085 } 086 return comp; 087 } 088 089 public void removeEditor() { 090 KeyboardFocusManager.getCurrentKeyboardFocusManager(). 091 removePropertyChangeListener("permanentFocusOwner", editorRemover); // NOI18N 092 editorRemover = null; 093 094 if (cellEditor != null) { 095 cellEditor.removeCellEditorListener(this); 096 097 if (editorComp != null) { 098 remove(editorComp); 099 } 100 101 Rectangle cellRect = getCellBounds(editingIndex, editingIndex); 102 103 editingIndex = -1; 104 editorComp = null; 105 106 repaint(cellRect); 107 } 108 } 109 110 public boolean editCellAt(int index, EventObject e) { 111 if (cellEditor != null && !cellEditor.stopCellEditing()) { 112 return false; 113 } 114 115 if (index < 0 || index >= getModel().getSize()) { 116 return false; 117 } 118 119 if (!isCellEditable(index)) { 120 return false; 121 } 122 123 if (editorRemover == null) { 124 KeyboardFocusManager fm = KeyboardFocusManager.getCurrentKeyboardFocusManager(); 125 editorRemover = new CellEditorRemover(fm); 126 fm.addPropertyChangeListener("permanentFocusOwner", editorRemover); // NOI18N 127 } 128 129 if (cellEditor != null && cellEditor.isCellEditable(e)) { 130 editorComp = prepareEditor(index); 131 if (editorComp == null) { 132 removeEditor(); 133 return false; 134 } 135 editorComp.setBounds(getCellBounds(index, index)); 136 add(editorComp); 137 editorComp.revalidate(); 138 139 editingIndex = index; 140 cellEditor.addCellEditorListener(this); 141 142 return true; 143 } 144 return false; 145 } 146 147 @Override 148 public void removeNotify() { 149 KeyboardFocusManager.getCurrentKeyboardFocusManager(). 150 removePropertyChangeListener("permanentFocusOwner", editorRemover); // NOI18N 151 super.removeNotify(); 152 } 153 154 // This class tracks changes in the keyboard focus state. It is used 155 // when the XList is editing to determine when to cancel the edit. 156 // If focus switches to a component outside of the XList, but in the 157 // same window, this will cancel editing. 158 class CellEditorRemover implements PropertyChangeListener { 159 160 KeyboardFocusManager focusManager; 161 162 public CellEditorRemover(KeyboardFocusManager fm) { 163 this.focusManager = fm; 164 } 165 166 @Override 167 public void propertyChange(PropertyChangeEvent ev) { 168 if (!isEditing() || !getClientProperty("terminateEditOnFocusLost").equals(Boolean.TRUE) ) { // NOI18N 169 return; 170 } 171 172 Component c = focusManager.getPermanentFocusOwner(); 173 while (c != null) { 174 if (c == EditableList.this) { 175 // focus remains inside the table 176 return; 177 } else if (c instanceof Window) { 178 if (c == SwingUtilities.getRoot(EditableList.this)) { 179 if (!getListCellEditor().stopCellEditing()) { 180 getListCellEditor().cancelCellEditing(); 181 } 182 } 183 break; 184 } 185 c = c.getParent(); 186 } 187 } 188 } 189 190 /* 191 * Model Support 192 */ 193 public boolean isCellEditable(int index) { 194 if (getModel() instanceof EditableListModel) { 195 return ((EditableListModel<E>) getModel()).isCellEditable(index); 196 } 197 return false; 198 } 199 200 public void setValueAt(E value, int index) { 201 ((EditableListModel<E>) getModel()).setValueAt(value, index); 202 } 203 204 /* 205 * CellEditorListener 206 */ 207 @Override 208 public void editingStopped(ChangeEvent e) { 209 if (cellEditor != null) { 210 E value = cellEditor.getCellEditorValue(); 211 setValueAt(value, editingIndex); 212 removeEditor(); 213 } 214 } 215 216 @Override 217 public void editingCanceled(ChangeEvent e) { 218 removeEditor(); 219 } 220 221 /* 222 * Editing 223 */ 224 private class StartEditingAction extends AbstractAction { 225 226 @Override 227 @SuppressWarnings("unchecked") // have to cast CellEditor to ListCellEditor to access methods 228 public void actionPerformed(ActionEvent e) { 229 EditableList<E> list = (EditableList<E>) e.getSource(); 230 if (!list.hasFocus()) { 231 ListCellEditor<E> cellEditor = list.getListCellEditor(); 232 if (cellEditor != null && !cellEditor.stopCellEditing()) { 233 return; 234 } 235 list.requestFocus(); 236 return; 237 } 238 ListSelectionModel rsm = list.getSelectionModel(); 239 int anchorRow = rsm.getAnchorSelectionIndex(); 240 list.editCellAt(anchorRow, null); 241 Component editorComp = list.getEditorComponent(); 242 if (editorComp != null) { 243 editorComp.requestFocus(); 244 } 245 } 246 } 247 248 private class CancelEditingAction extends AbstractAction { 249 250 @SuppressWarnings("unchecked") 251 @Override 252 public void actionPerformed(ActionEvent e) { 253 EditableList<E> list = (EditableList<E>) e.getSource(); 254 list.removeEditor(); 255 } 256 257 @Override 258 public boolean isEnabled() { 259 return isEditing(); 260 } 261 } 262 263 private class MouseListener extends MouseAdapter { 264 265 private Component dispatchComponent; 266 267 private void setDispatchComponent(MouseEvent e) { 268 Component editorComponent = getEditorComponent(); 269 Point p = e.getPoint(); 270 Point p2 = SwingUtilities.convertPoint(EditableList.this, p, editorComponent); 271 dispatchComponent = SwingUtilities.getDeepestComponentAt(editorComponent, 272 p2.x, p2.y); 273 } 274 275 private boolean repostEvent(MouseEvent e) { 276 // Check for isEditing() in case another event has 277 // caused the cellEditor to be removed. See bug #4306499. 278 if (dispatchComponent == null || !isEditing()) { 279 return false; 280 } 281 MouseEvent e2 = SwingUtilities.convertMouseEvent(EditableList.this, e, dispatchComponent); 282 dispatchComponent.dispatchEvent(e2); 283 return true; 284 } 285 286 private boolean shouldIgnore(MouseEvent e) { 287 return e.isConsumed() || (!(SwingUtilities.isLeftMouseButton(e) && isEnabled())); 288 } 289 290 @Override 291 public void mousePressed(MouseEvent e) { 292 if (shouldIgnore(e)) { 293 return; 294 } 295 Point p = e.getPoint(); 296 int index = locationToIndex(p); 297 // The autoscroller can generate drag events outside the Table's range. 298 if (index == -1) { 299 return; 300 } 301 302 if (editCellAt(index, e)) { 303 setDispatchComponent(e); 304 repostEvent(e); 305 } else if (isRequestFocusEnabled()) { 306 requestFocus(); 307 } 308 } 309 } 310}