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