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