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