001    // License: GPL. For details, see LICENSE file.
002    package org.openstreetmap.josm.gui.tagging;
003    
004    import static org.openstreetmap.josm.tools.I18n.tr;
005    
006    import java.applet.Applet;
007    import java.awt.AWTException;
008    import java.awt.Component;
009    import java.awt.Container;
010    import java.awt.Dimension;
011    import java.awt.KeyboardFocusManager;
012    import java.awt.MouseInfo;
013    import java.awt.Point;
014    import java.awt.Rectangle;
015    import java.awt.Robot;
016    import java.awt.Window;
017    import java.awt.event.ActionEvent;
018    import java.awt.event.InputEvent;
019    import java.awt.event.KeyEvent;
020    import java.awt.event.KeyListener;
021    import java.beans.PropertyChangeEvent;
022    import java.beans.PropertyChangeListener;
023    import java.util.EventObject;
024    import java.util.concurrent.CopyOnWriteArrayList;
025    
026    import javax.swing.AbstractAction;
027    import javax.swing.CellEditor;
028    import javax.swing.DefaultListSelectionModel;
029    import javax.swing.JComponent;
030    import javax.swing.JTable;
031    import javax.swing.JViewport;
032    import javax.swing.KeyStroke;
033    import javax.swing.ListSelectionModel;
034    import javax.swing.SwingUtilities;
035    import javax.swing.event.ListSelectionEvent;
036    import javax.swing.event.ListSelectionListener;
037    import javax.swing.table.DefaultTableColumnModel;
038    import javax.swing.table.TableColumn;
039    
040    import org.openstreetmap.josm.gui.dialogs.relation.RunnableAction;
041    import org.openstreetmap.josm.gui.tagging.ac.AutoCompletionManager;
042    import org.openstreetmap.josm.gui.tagging.ac.AutoCompletionList;
043    import org.openstreetmap.josm.tools.ImageProvider;
044    
045    /**
046     * This is the tabular editor component for OSM tags.
047     *
048     */
049    public class TagTable extends JTable  {
050        /** the table cell editor used by this table */
051        private TagCellEditor editor = null;
052    
053        /** a list of components to which focus can be transferred without stopping
054         * cell editing this table.
055         */
056        private final CopyOnWriteArrayList<Component> doNotStopCellEditingWhenFocused = new CopyOnWriteArrayList<Component>();
057        private CellEditorRemover editorRemover;
058    
059        /**
060         * The table has two columns. The first column is used for editing rendering and
061         * editing tag keys, the second for rendering and editing tag values.
062         *
063         */
064        static class TagTableColumnModel extends DefaultTableColumnModel {
065            public TagTableColumnModel(DefaultListSelectionModel selectionModel) {
066                setSelectionModel(selectionModel);
067                TableColumn col = null;
068                TagCellRenderer renderer = new TagCellRenderer();
069    
070                // column 0 - tag key
071                col = new TableColumn(0);
072                col.setHeaderValue(tr("Key"));
073                col.setResizable(true);
074                col.setCellRenderer(renderer);
075                addColumn(col);
076    
077                // column 1 - tag value
078                col = new TableColumn(1);
079                col.setHeaderValue(tr("Value"));
080                col.setResizable(true);
081                col.setCellRenderer(renderer);
082                addColumn(col);
083            }
084        }
085    
086        /**
087         * Action to be run when the user navigates to the next cell in the table,
088         * for instance by pressing TAB or ENTER. The action alters the standard
089         * navigation path from cell to cell:
090         * <ul>
091         *   <li>it jumps over cells in the first column</li>
092         *   <li>it automatically add a new empty row when the user leaves the
093         *   last cell in the table</li>
094         * <ul>
095         *
096         */
097        class SelectNextColumnCellAction extends AbstractAction  {
098            public void actionPerformed(ActionEvent e) {
099                run();
100            }
101    
102            public void run() {
103                int col = getSelectedColumn();
104                int row = getSelectedRow();
105                if (getCellEditor() != null) {
106                    getCellEditor().stopCellEditing();
107                }
108    
109                if (col == 0) {
110                    col++;
111                } else if (col == 1 && row < getRowCount()-1) {
112                    col=0;
113                    row++;
114                } else if (col == 1 && row == getRowCount()-1){
115                    // we are at the end. Append an empty row and move the focus
116                    // to its second column
117                    TagEditorModel model = (TagEditorModel)getModel();
118                    model.appendNewTag();
119                    col=0;
120                    row++;
121                }
122                changeSelection(row, col, false, false);
123                requestFocusInCell(row,col);
124            }
125        }
126    
127        /**
128         * Action to be run when the user navigates to the previous cell in the table,
129         * for instance by pressing Shift-TAB
130         *
131         */
132        class SelectPreviousColumnCellAction extends AbstractAction  {
133    
134            public void actionPerformed(ActionEvent e) {
135                int col = getSelectedColumn();
136                int row = getSelectedRow();
137                if (getCellEditor() != null) {
138                    getCellEditor().stopCellEditing();
139                }
140    
141                if (col <= 0 && row <= 0) {
142                    // change nothing
143                } else if (col == 1) {
144                    col--;
145                } else {
146                    col = 1;
147                    row--;
148                }
149                changeSelection(row, col, false, false);
150                requestFocusInCell(row,col);
151            }
152        }
153    
154        /**
155         * Action to be run when the user invokes a delete action on the table, for
156         * instance by pressing DEL.
157         *
158         * Depending on the shape on the current selection the action deletes individual
159         * values or entire tags from the model.
160         *
161         * If the current selection consists of cells in the second column only, the keys of
162         * the selected tags are set to the empty string.
163         *
164         * If the current selection consists of cell in the third column only, the values of the
165         * selected tags are set to the empty string.
166         *
167         *  If the current selection consists of cells in the second and the third column,
168         *  the selected tags are removed from the model.
169         *
170         *  This action listens to the table selection. It becomes enabled when the selection
171         *  is non-empty, otherwise it is disabled.
172         *
173         *
174         */
175        class DeleteAction extends RunnableAction implements ListSelectionListener {
176    
177            public DeleteAction() {
178                putValue(SMALL_ICON, ImageProvider.get("dialogs", "delete"));
179                putValue(SHORT_DESCRIPTION, tr("Delete the selection in the tag table"));
180                getSelectionModel().addListSelectionListener(this);
181                getColumnModel().getSelectionModel().addListSelectionListener(this);
182                updateEnabledState();
183            }
184    
185            /**
186             * delete a selection of tag names
187             */
188            protected void deleteTagNames() {
189                int[] rows = getSelectedRows();
190                TagEditorModel model = (TagEditorModel)getModel();
191                model.deleteTagNames(rows);
192            }
193    
194            /**
195             * delete a selection of tag values
196             */
197            protected void deleteTagValues() {
198                int[] rows = getSelectedRows();
199                TagEditorModel model = (TagEditorModel)getModel();
200                model.deleteTagValues(rows);
201            }
202    
203            /**
204             * delete a selection of tags
205             */
206            protected void deleteTags() {
207                int[] rows = getSelectedRows();
208                TagEditorModel model = (TagEditorModel)getModel();
209                model.deleteTags(rows);
210            }
211    
212            @Override
213            public void run() {
214                if (!isEnabled())
215                    return;
216                switch(getSelectedColumnCount()) {
217                case 1:
218                    if (getSelectedColumn() == 0) {
219                        deleteTagNames();
220                    } else if (getSelectedColumn() == 1) {
221                        deleteTagValues();
222                    }
223                    break;
224                case 2:
225                    deleteTags();
226                    break;
227                }
228    
229                if (isEditing()) {
230                    CellEditor editor = getCellEditor();
231                    if (editor != null) {
232                        editor.cancelCellEditing();
233                    }
234                }
235    
236                TagEditorModel model = (TagEditorModel)getModel();
237                if (model.getRowCount() == 0) {
238                    model.ensureOneTag();
239                    requestFocusInCell(0, 0);
240                }
241            }
242    
243            /**
244             * listens to the table selection model
245             */
246            public void valueChanged(ListSelectionEvent e) {
247                updateEnabledState();
248            }
249    
250            protected void updateEnabledState() {
251                if (isEditing() && getSelectedColumnCount() == 1 && getSelectedRowCount() == 1) {
252                    setEnabled(true);
253                } else if (!isEditing() && getSelectedColumnCount() == 1 && getSelectedRowCount() == 1) {
254                    setEnabled(true);
255                } else if (getSelectedColumnCount() > 1 || getSelectedRowCount() > 1) {
256                    setEnabled(true);
257                } else {
258                    setEnabled(false);
259                }
260            }
261        }
262    
263        /**
264         * Action to be run when the user adds a new tag.
265         *
266         *
267         */
268        class AddAction extends RunnableAction implements PropertyChangeListener{
269            public AddAction() {
270                putValue(SMALL_ICON, ImageProvider.get("dialogs", "add"));
271                putValue(SHORT_DESCRIPTION, tr("Add a new tag"));
272                TagTable.this.addPropertyChangeListener(this);
273                updateEnabledState();
274            }
275    
276            @Override
277            public void run() {
278                CellEditor editor = getCellEditor();
279                if (editor != null) {
280                    getCellEditor().stopCellEditing();
281                }
282                ((TagEditorModel)getModel()).appendNewTag();
283                final int rowIdx = getModel().getRowCount()-1;
284                requestFocusInCell(rowIdx, 0);
285            }
286    
287            protected void updateEnabledState() {
288                setEnabled(TagTable.this.isEnabled());
289            }
290    
291            public void propertyChange(PropertyChangeEvent evt) {
292                updateEnabledState();
293            }
294        }
295    
296        /** the delete action */
297        private RunnableAction deleteAction = null;
298    
299        /** the add action */
300        private RunnableAction addAction = null;
301    
302        /**
303         *
304         * @return the delete action used by this table
305         */
306        public RunnableAction getDeleteAction() {
307            return deleteAction;
308        }
309    
310        public RunnableAction getAddAction() {
311            return addAction;
312        }
313    
314        /**
315         * initialize the table
316         */
317        protected void init() {
318            setAutoResizeMode(JTable.AUTO_RESIZE_OFF);
319            //setCellSelectionEnabled(true);
320            setRowSelectionAllowed(true);
321            setColumnSelectionAllowed(true);
322            setSelectionMode(ListSelectionModel.SINGLE_INTERVAL_SELECTION);
323    
324            // make ENTER behave like TAB
325            //
326            getInputMap(JComponent.WHEN_ANCESTOR_OF_FOCUSED_COMPONENT)
327            .put(KeyStroke.getKeyStroke(KeyEvent.VK_ENTER, 0, false), "selectNextColumnCell");
328    
329            // install custom navigation actions
330            //
331            getActionMap().put("selectNextColumnCell", new SelectNextColumnCellAction());
332            getActionMap().put("selectPreviousColumnCell", new SelectPreviousColumnCellAction());
333    
334            // create a delete action. Installing this action in the input and action map
335            // didn't work. We therefore handle delete requests in processKeyBindings(...)
336            //
337            deleteAction = new DeleteAction();
338    
339            // create the add action
340            //
341            addAction = new AddAction();
342            getInputMap(JComponent.WHEN_ANCESTOR_OF_FOCUSED_COMPONENT)
343            .put(KeyStroke.getKeyStroke(KeyEvent.VK_ADD, KeyEvent.CTRL_MASK), "addTag");
344            getActionMap().put("addTag", addAction);
345    
346            // create the table cell editor and set it to key and value columns
347            //
348            TagCellEditor tmpEditor = new TagCellEditor();
349            setRowHeight(tmpEditor.getEditor().getPreferredSize().height);
350            setTagCellEditor(tmpEditor);
351        }
352    
353        /**
354         * Creates a new tag table
355         *
356         * @param model the tag editor model
357         */
358        public TagTable(TagEditorModel model) {
359            super(model, new TagTableColumnModel(model.getColumnSelectionModel()), model.getRowSelectionModel());
360            init();
361        }
362    
363        @Override
364        public Dimension getPreferredSize(){
365            Container c = getParent();
366            while(c != null && ! (c instanceof JViewport)) {
367                c = c.getParent();
368            }
369            if (c != null) {
370                Dimension d = super.getPreferredSize();
371                d.width = c.getSize().width;
372                return d;
373            }
374            return super.getPreferredSize();
375        }
376    
377        @Override protected boolean processKeyBinding(KeyStroke ks, KeyEvent e,
378                int condition, boolean pressed) {
379    
380            // handle delete key
381            //
382            if (e.getKeyCode() == KeyEvent.VK_DELETE) {
383                if (isEditing() && getSelectedColumnCount() == 1 && getSelectedRowCount() == 1)
384                    // if DEL was pressed and only the currently edited cell is selected,
385                    // don't run the delete action. DEL is handled by the CellEditor as normal
386                    // DEL in the text input.
387                    //
388                    return super.processKeyBinding(ks, e, condition, pressed);
389                getDeleteAction().run();
390            }
391            return super.processKeyBinding(ks, e, condition, pressed);
392        }
393    
394        /**
395         * @param autoCompletionList
396         */
397        public void setAutoCompletionList(AutoCompletionList autoCompletionList) {
398            if (autoCompletionList == null)
399                return;
400            if (editor != null) {
401                editor.setAutoCompletionList(autoCompletionList);
402            }
403        }
404    
405        public void setAutoCompletionManager(AutoCompletionManager autocomplete) {
406            if (autocomplete == null) {
407                System.out.println("argument autocomplete should not be null. Aborting.");
408                Thread.dumpStack();
409                return;
410            }
411            if (editor != null) {
412                editor.setAutoCompletionManager(autocomplete);
413            }
414        }
415    
416        public AutoCompletionList getAutoCompletionList() {
417            if (editor != null)
418                return editor.getAutoCompletionList();
419            else
420                return null;
421        }
422    
423        public TagCellEditor getTableCellEditor() {
424            return editor;
425        }
426    
427        public void  addOKAccelatorListener(KeyListener l) {
428            addKeyListener(l);
429            if (editor != null) {
430                editor.getEditor().addKeyListener(l);
431            }
432        }
433    
434        /**
435         * Inject a tag cell editor in the tag table
436         *
437         * @param editor
438         */
439        public void setTagCellEditor(TagCellEditor editor) {
440            if (isEditing()) {
441                this.editor.cancelCellEditing();
442            }
443            this.editor = editor;
444            getColumnModel().getColumn(0).setCellEditor(editor);
445            getColumnModel().getColumn(1).setCellEditor(editor);
446        }
447    
448        public void requestFocusInCell(final int row, final int col) {
449    
450            // the following code doesn't work reliably. If a table cell
451            // gains focus using editCellAt() and requestFocusInWindow()
452            // it isn't possible to tab to the next table cell using TAB or
453            // ENTER. Don't know why.
454            //
455            // tblTagEditor.editCellAt(row, col);
456            // if (tblTagEditor.getEditorComponent() != null) {
457            //  tblTagEditor.getEditorComponent().requestFocusInWindow();
458            // }
459    
460            // this is a workaround. We move the focus to the respective cell
461            // using a simulated mouse click. In this case one can tab out of
462            // the cell using TAB and ENTER.
463            //
464            Rectangle r = getCellRect(row,col, false);
465            Point p = new Point(r.x + r.width/2, r.y + r.height/2);
466            SwingUtilities.convertPointToScreen(p, this);
467            Point before = MouseInfo.getPointerInfo().getLocation();
468    
469            try {
470                Robot robot = new Robot();
471                robot.mouseMove(p.x,p.y);
472                robot.mousePress(InputEvent.BUTTON1_MASK);
473                robot.mouseRelease(InputEvent.BUTTON1_MASK);
474                robot.mouseMove(before.x, before.y);
475            } catch(AWTException e) {
476                System.out.println("Failed to simulate mouse click event at (" + r.x + "," + r.y + "). Exception: " + e.toString());
477                return;
478            }
479        }
480    
481        public void addComponentNotStoppingCellEditing(Component component) {
482            if (component == null) return;
483            doNotStopCellEditingWhenFocused.addIfAbsent(component);
484        }
485    
486        public void removeComponentNotStoppingCellEditing(Component component) {
487            if (component == null) return;
488            doNotStopCellEditingWhenFocused.remove(component);
489        }
490    
491        @Override
492        public boolean editCellAt(int row, int column, EventObject e){
493    
494            // a snipped copied from the Java 1.5 implementation of JTable
495            //
496            if (cellEditor != null && !cellEditor.stopCellEditing())
497                return false;
498    
499            if (row < 0 || row >= getRowCount() ||
500                    column < 0 || column >= getColumnCount())
501                return false;
502    
503            if (!isCellEditable(row, column))
504                return false;
505    
506            // make sure our custom implementation of CellEditorRemover is created
507            if (editorRemover == null) {
508                KeyboardFocusManager fm =
509                    KeyboardFocusManager.getCurrentKeyboardFocusManager();
510                editorRemover = new CellEditorRemover(fm);
511                fm.addPropertyChangeListener("permanentFocusOwner", editorRemover);
512            }
513    
514            // delegate to the default implementation
515            return super.editCellAt(row, column,e);
516        }
517    
518    
519        @Override
520        public void removeEditor() {
521            // make sure we unregister our custom implementation of CellEditorRemover
522            KeyboardFocusManager.getCurrentKeyboardFocusManager().
523            removePropertyChangeListener("permanentFocusOwner", editorRemover);
524            editorRemover = null;
525            super.removeEditor();
526        }
527    
528        @Override
529        public void removeNotify() {
530            // make sure we unregister our custom implementation of CellEditorRemover
531            KeyboardFocusManager.getCurrentKeyboardFocusManager().
532            removePropertyChangeListener("permanentFocusOwner", editorRemover);
533            editorRemover = null;
534            super.removeNotify();
535        }
536    
537        /**
538         * This is a custom implementation of the CellEditorRemover used in JTable
539         * to handle the client property <tt>terminateEditOnFocusLost</tt>.
540         *
541         * This implementation also checks whether focus is transferred to one of a list
542         * of dedicated components, see {@link TagTable#doNotStopCellEditingWhenFocused}.
543         * A typical example for such a component is a button in {@link TagEditorPanel}
544         * which isn't a child component of {@link TagTable} but which should respond to
545         * to focus transfer in a similar way to a child of TagTable.
546         *
547         */
548        class CellEditorRemover implements PropertyChangeListener {
549            KeyboardFocusManager focusManager;
550    
551            public CellEditorRemover(KeyboardFocusManager fm) {
552                this.focusManager = fm;
553            }
554    
555            public void propertyChange(PropertyChangeEvent ev) {
556                if (!isEditing())
557                    return;
558    
559                Component c = focusManager.getPermanentFocusOwner();
560                while (c != null) {
561                    if (c == TagTable.this)
562                        // focus remains inside the table
563                        return;
564                    if (doNotStopCellEditingWhenFocused.contains(c))
565                        // focus remains on one of the associated components
566                        return;
567                    else if ((c instanceof Window) ||
568                            (c instanceof Applet && c.getParent() == null)) {
569                        if (c == SwingUtilities.getRoot(TagTable.this)) {
570                            if (!getCellEditor().stopCellEditing()) {
571                                getCellEditor().cancelCellEditing();
572                            }
573                        }
574                        break;
575                    }
576                    c = c.getParent();
577                }
578            }
579        }
580    }