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