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}