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