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 = null;
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 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        public 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
132                // to its second column
133                String key = ((TagModel)model.getValueAt(row, 0)).getName();
134                if (!key.trim().isEmpty()) {
135                    model.appendNewTag();
136                    col=0;
137                    row++;
138                } else {
139                    clearSelection();
140                    if (nextFocusComponent!=null)
141                        nextFocusComponent.requestFocusInWindow();
142                    return;
143                }
144            }
145            requestFocusInCell(row,col);
146        }
147    }
148
149    /**
150     * Action to be run when the user navigates to the previous cell in the table,
151     * for instance by pressing Shift-TAB
152     *
153     */
154    class SelectPreviousColumnCellAction extends AbstractAction  {
155
156        @Override
157        public void actionPerformed(ActionEvent e) {
158            int col = getSelectedColumn();
159            int row = getSelectedRow();
160            if (getCellEditor() != null) {
161                getCellEditor().stopCellEditing();
162            }
163
164            if (col <= 0 && row <= 0) {
165                // change nothing
166            } else if (col == 1) {
167                col--;
168            } else {
169                col = 1;
170                row--;
171            }
172            requestFocusInCell(row,col);
173        }
174    }
175
176    /**
177     * Action to be run when the user invokes a delete action on the table, for
178     * instance by pressing DEL.
179     *
180     * Depending on the shape on the current selection the action deletes individual
181     * values or entire tags from the model.
182     *
183     * If the current selection consists of cells in the second column only, the keys of
184     * the selected tags are set to the empty string.
185     *
186     * If the current selection consists of cell in the third column only, the values of the
187     * selected tags are set to the empty string.
188     *
189     *  If the current selection consists of cells in the second and the third column,
190     *  the selected tags are removed from the model.
191     *
192     *  This action listens to the table selection. It becomes enabled when the selection
193     *  is non-empty, otherwise it is disabled.
194     *
195     *
196     */
197    class DeleteAction extends RunnableAction implements ListSelectionListener {
198
199        public DeleteAction() {
200            putValue(SMALL_ICON, ImageProvider.get("dialogs", "delete"));
201            putValue(SHORT_DESCRIPTION, tr("Delete the selection in the tag table"));
202            getSelectionModel().addListSelectionListener(this);
203            getColumnModel().getSelectionModel().addListSelectionListener(this);
204            updateEnabledState();
205        }
206
207        /**
208         * delete a selection of tag names
209         */
210        protected void deleteTagNames() {
211            int[] rows = getSelectedRows();
212            model.deleteTagNames(rows);
213        }
214
215        /**
216         * delete a selection of tag values
217         */
218        protected void deleteTagValues() {
219            int[] rows = getSelectedRows();
220            model.deleteTagValues(rows);
221        }
222
223        /**
224         * delete a selection of tags
225         */
226        protected void deleteTags() {
227            int[] rows = getSelectedRows();
228            model.deleteTags(rows);
229        }
230
231        @Override
232        public void run() {
233            if (!isEnabled())
234                return;
235            switch(getSelectedColumnCount()) {
236            case 1:
237                if (getSelectedColumn() == 0) {
238                    deleteTagNames();
239                } else if (getSelectedColumn() == 1) {
240                    deleteTagValues();
241                }
242                break;
243            case 2:
244                deleteTags();
245                break;
246            }
247
248            if (isEditing()) {
249                CellEditor editor = getCellEditor();
250                if (editor != null) {
251                    editor.cancelCellEditing();
252                }
253            }
254
255            if (model.getRowCount() == 0) {
256                model.ensureOneTag();
257                requestFocusInCell(0, 0);
258            }
259        }
260
261        /**
262         * listens to the table selection model
263         */
264        @Override
265        public void valueChanged(ListSelectionEvent e) {
266            updateEnabledState();
267        }
268
269        protected final void updateEnabledState() {
270            if (isEditing() && getSelectedColumnCount() == 1 && getSelectedRowCount() == 1) {
271                setEnabled(true);
272            } else if (!isEditing() && getSelectedColumnCount() == 1 && getSelectedRowCount() == 1) {
273                setEnabled(true);
274            } else if (getSelectedColumnCount() > 1 || getSelectedRowCount() > 1) {
275                setEnabled(true);
276            } else {
277                setEnabled(false);
278            }
279        }
280    }
281
282    /**
283     * Action to be run when the user adds a new tag.
284     *
285     *
286     */
287    class AddAction extends RunnableAction implements PropertyChangeListener{
288        public AddAction() {
289            putValue(SMALL_ICON, ImageProvider.get("dialogs", "add"));
290            putValue(SHORT_DESCRIPTION, tr("Add a new tag"));
291            TagTable.this.addPropertyChangeListener(this);
292            updateEnabledState();
293        }
294
295        @Override
296        public void run() {
297            CellEditor editor = getCellEditor();
298            if (editor != null) {
299                getCellEditor().stopCellEditing();
300            }
301            final int rowIdx = model.getRowCount()-1;
302            String key = ((TagModel)model.getValueAt(rowIdx, 0)).getName();
303            if (!key.trim().isEmpty()) {
304                model.appendNewTag();
305            }
306            requestFocusInCell(model.getRowCount()-1, 0);
307        }
308
309        protected final void updateEnabledState() {
310            setEnabled(TagTable.this.isEnabled());
311        }
312
313        @Override
314        public void propertyChange(PropertyChangeEvent evt) {
315            updateEnabledState();
316        }
317    }
318
319     /**
320     * Action to be run when the user wants to paste tags from buffer
321     */
322    class PasteAction extends RunnableAction implements PropertyChangeListener{
323        public PasteAction() {
324            putValue(SMALL_ICON, ImageProvider.get("","pastetags"));
325            putValue(SHORT_DESCRIPTION, tr("Paste tags from buffer"));
326            TagTable.this.addPropertyChangeListener(this);
327            updateEnabledState();
328        }
329
330        @Override
331        public void run() {
332            Relation relation = new Relation();
333            model.applyToPrimitive(relation);
334
335            String buf = Utils.getClipboardContent();
336            if (buf == null || buf.isEmpty() || buf.matches(CopyAction.CLIPBOARD_REGEXP)) {
337                List<PrimitiveData> directlyAdded = Main.pasteBuffer.getDirectlyAdded();
338                if (directlyAdded==null || directlyAdded.isEmpty()) return;
339                PasteTagsAction.TagPaster tagPaster = new PasteTagsAction.TagPaster(directlyAdded, Collections.<OsmPrimitive>singletonList(relation));
340                model.updateTags(tagPaster.execute());
341            } else {
342                // Paste tags from arbitrary text
343                 Map<String, String> tags = TextTagParser.readTagsFromText(buf);
344                 if (tags==null || tags.isEmpty()) {
345                    TextTagParser.showBadBufferMessage(ht("/Action/PasteTags"));
346                 } else if (TextTagParser.validateTags(tags)) {
347                     List<Tag> newTags = new ArrayList<>();
348                     for (Map.Entry<String, String> entry: tags.entrySet()) {
349                        String k = entry.getKey();
350                        String v = entry.getValue();
351                        newTags.add(new Tag(k,v));
352                     }
353                     model.updateTags(newTags);
354                 }
355            }
356        }
357
358        protected final void updateEnabledState() {
359            setEnabled(TagTable.this.isEnabled());
360        }
361
362        @Override
363        public void propertyChange(PropertyChangeEvent evt) {
364            updateEnabledState();
365        }
366    }
367
368    /** the delete action */
369    private RunnableAction deleteAction = null;
370
371    /** the add action */
372    private RunnableAction addAction = null;
373
374    /** the tag paste action */
375    private RunnableAction pasteAction = null;
376
377    /**
378     *
379     * @return the delete action used by this table
380     */
381    public RunnableAction getDeleteAction() {
382        return deleteAction;
383    }
384
385    public RunnableAction getAddAction() {
386        return addAction;
387    }
388
389    public RunnableAction getPasteAction() {
390        return pasteAction;
391    }
392
393    /**
394     * initialize the table
395     */
396    protected final void init() {
397        setAutoResizeMode(JTable.AUTO_RESIZE_OFF);
398        setRowSelectionAllowed(true);
399        setColumnSelectionAllowed(true);
400        setSelectionMode(ListSelectionModel.SINGLE_INTERVAL_SELECTION);
401
402        // make ENTER behave like TAB
403        //
404        getInputMap(JComponent.WHEN_ANCESTOR_OF_FOCUSED_COMPONENT)
405        .put(KeyStroke.getKeyStroke(KeyEvent.VK_ENTER, 0, false), "selectNextColumnCell");
406
407        // install custom navigation actions
408        //
409        getActionMap().put("selectNextColumnCell", new SelectNextColumnCellAction());
410        getActionMap().put("selectPreviousColumnCell", new SelectPreviousColumnCellAction());
411
412        // create a delete action. Installing this action in the input and action map
413        // didn't work. We therefore handle delete requests in processKeyBindings(...)
414        //
415        deleteAction = new DeleteAction();
416
417        // create the add action
418        //
419        addAction = new AddAction();
420        getInputMap(JComponent.WHEN_ANCESTOR_OF_FOCUSED_COMPONENT)
421        .put(KeyStroke.getKeyStroke(KeyEvent.VK_ADD, KeyEvent.CTRL_MASK), "addTag");
422        getActionMap().put("addTag", addAction);
423
424        pasteAction = new PasteAction();
425
426        // create the table cell editor and set it to key and value columns
427        //
428        TagCellEditor tmpEditor = new TagCellEditor();
429        setRowHeight(tmpEditor.getEditor().getPreferredSize().height);
430        setTagCellEditor(tmpEditor);
431    }
432
433    /**
434     * Creates a new tag table
435     *
436     * @param model the tag editor model
437     */
438    public TagTable(TagEditorModel model) {
439        super(model, new TagTableColumnModel(model.getColumnSelectionModel()), model.getRowSelectionModel());
440        this.model = model;
441        init();
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     * @param autoCompletionList
477     */
478    public void setAutoCompletionList(AutoCompletionList autoCompletionList) {
479        if (autoCompletionList == null)
480            return;
481        if (editor != null) {
482            editor.setAutoCompletionList(autoCompletionList);
483        }
484    }
485
486    public void setAutoCompletionManager(AutoCompletionManager autocomplete) {
487        if (autocomplete == null) {
488            Main.warn("argument autocomplete should not be null. Aborting.");
489            Thread.dumpStack();
490            return;
491        }
492        if (editor != null) {
493            editor.setAutoCompletionManager(autocomplete);
494        }
495    }
496
497    public AutoCompletionList getAutoCompletionList() {
498        if (editor != null)
499            return editor.getAutoCompletionList();
500        else
501            return null;
502    }
503
504    public void setNextFocusComponent(Component nextFocusComponent) {
505        this.nextFocusComponent = nextFocusComponent;
506    }
507
508    public TagCellEditor getTableCellEditor() {
509        return editor;
510    }
511
512    public void  addOKAccelatorListener(KeyListener l) {
513        addKeyListener(l);
514        if (editor != null) {
515            editor.getEditor().addKeyListener(l);
516        }
517    }
518
519    /**
520     * Inject a tag cell editor in the tag table
521     *
522     * @param editor
523     */
524    public void setTagCellEditor(TagCellEditor editor) {
525        if (isEditing()) {
526            this.editor.cancelCellEditing();
527        }
528        this.editor = editor;
529        getColumnModel().getColumn(0).setCellEditor(editor);
530        getColumnModel().getColumn(1).setCellEditor(editor);
531    }
532
533    public void requestFocusInCell(final int row, final int col) {
534        changeSelection(row, col, false, false);
535        editCellAt(row, col);
536        Component c = getEditorComponent();
537        if (c!=null) {
538            c.requestFocusInWindow();
539            if ( c instanceof JTextComponent ) {
540                 ( (JTextComponent)c ).selectAll();
541            }
542        }
543        // there was a bug here - on older 1.6 Java versions Tab was not working
544        // after such activation. In 1.7 it works OK,
545        // previous solution of usint awt.Robot was resetting mouse speed on Windows
546    }
547
548    public void addComponentNotStoppingCellEditing(Component component) {
549        if (component == null) return;
550        doNotStopCellEditingWhenFocused.addIfAbsent(component);
551    }
552
553    public void removeComponentNotStoppingCellEditing(Component component) {
554        if (component == null) return;
555        doNotStopCellEditingWhenFocused.remove(component);
556    }
557
558    @Override
559    public boolean editCellAt(int row, int column, EventObject e){
560
561        // a snipped copied from the Java 1.5 implementation of JTable
562        //
563        if (cellEditor != null && !cellEditor.stopCellEditing())
564            return false;
565
566        if (row < 0 || row >= getRowCount() ||
567                column < 0 || column >= getColumnCount())
568            return false;
569
570        if (!isCellEditable(row, column))
571            return false;
572
573        // make sure our custom implementation of CellEditorRemover is created
574        if (editorRemover == null) {
575            KeyboardFocusManager fm =
576                KeyboardFocusManager.getCurrentKeyboardFocusManager();
577            editorRemover = new CellEditorRemover(fm);
578            fm.addPropertyChangeListener("permanentFocusOwner", editorRemover);
579        }
580
581        // delegate to the default implementation
582        return super.editCellAt(row, column,e);
583    }
584
585
586    @Override
587    public void removeEditor() {
588        // make sure we unregister our custom implementation of CellEditorRemover
589        KeyboardFocusManager.getCurrentKeyboardFocusManager().
590        removePropertyChangeListener("permanentFocusOwner", editorRemover);
591        editorRemover = null;
592        super.removeEditor();
593    }
594
595    @Override
596    public void removeNotify() {
597        // make sure we unregister our custom implementation of CellEditorRemover
598        KeyboardFocusManager.getCurrentKeyboardFocusManager().
599        removePropertyChangeListener("permanentFocusOwner", editorRemover);
600        editorRemover = null;
601        super.removeNotify();
602    }
603
604    /**
605     * This is a custom implementation of the CellEditorRemover used in JTable
606     * to handle the client property <tt>terminateEditOnFocusLost</tt>.
607     *
608     * This implementation also checks whether focus is transferred to one of a list
609     * of dedicated components, see {@link TagTable#doNotStopCellEditingWhenFocused}.
610     * A typical example for such a component is a button in {@link TagEditorPanel}
611     * which isn't a child component of {@link TagTable} but which should respond to
612     * to focus transfer in a similar way to a child of TagTable.
613     *
614     */
615    class CellEditorRemover implements PropertyChangeListener {
616        KeyboardFocusManager focusManager;
617
618        public CellEditorRemover(KeyboardFocusManager fm) {
619            this.focusManager = fm;
620        }
621
622        @Override
623        public void propertyChange(PropertyChangeEvent ev) {
624            if (!isEditing())
625                return;
626
627            Component c = focusManager.getPermanentFocusOwner();
628            while (c != null) {
629                if (c == TagTable.this)
630                    // focus remains inside the table
631                    return;
632                if (doNotStopCellEditingWhenFocused.contains(c))
633                    // focus remains on one of the associated components
634                    return;
635                else if (c instanceof Window) {
636                    if (c == SwingUtilities.getRoot(TagTable.this)) {
637                        if (!getCellEditor().stopCellEditing()) {
638                            getCellEditor().cancelCellEditing();
639                        }
640                    }
641                    break;
642                }
643                c = c.getParent();
644            }
645        }
646    }
647}