001// License: GPL. For details, see LICENSE file.
002package org.openstreetmap.josm.gui.tagging;
003
004import java.awt.BorderLayout;
005import java.awt.Component;
006import java.awt.GridBagConstraints;
007import java.awt.GridBagLayout;
008import java.awt.Insets;
009import java.awt.event.FocusAdapter;
010import java.awt.event.FocusEvent;
011import java.util.Collections;
012
013import javax.swing.AbstractAction;
014import javax.swing.BoxLayout;
015import javax.swing.JButton;
016import javax.swing.JComponent;
017import javax.swing.JPanel;
018import javax.swing.JScrollPane;
019
020import org.openstreetmap.josm.data.osm.OsmPrimitive;
021import org.openstreetmap.josm.gui.dialogs.properties.HelpAction;
022import org.openstreetmap.josm.gui.dialogs.properties.HelpTagAction;
023import org.openstreetmap.josm.gui.dialogs.properties.PresetListPanel;
024import org.openstreetmap.josm.gui.layer.OsmDataLayer;
025import org.openstreetmap.josm.gui.tagging.ac.AutoCompletionList;
026import org.openstreetmap.josm.gui.tagging.ac.AutoCompletionManager;
027import org.openstreetmap.josm.gui.tagging.presets.TaggingPresetHandler;
028import org.openstreetmap.josm.spi.preferences.Config;
029import org.openstreetmap.josm.tools.CheckParameterUtil;
030
031/**
032 * TagEditorPanel is a {@link JPanel} which can be embedded as UI component in
033 * UIs. It provides a spreadsheet like tabular control for editing tag names
034 * and tag values. Two action buttons are placed on the left, one for adding
035 * a new tag and one for deleting the currently selected tags.
036 * @since 2040
037 */
038public class TagEditorPanel extends JPanel {
039    /** the tag editor model */
040    private TagEditorModel model;
041    /** the tag table */
042    private final TagTable tagTable;
043
044    private PresetListPanel presetListPanel;
045    private final transient TaggingPresetHandler presetHandler;
046
047    /**
048     * builds the panel with the table for editing tags
049     *
050     * @return the panel
051     */
052    protected JPanel buildTagTableEditorPanel() {
053        JPanel pnl = new JPanel(new BorderLayout());
054        pnl.add(new JScrollPane(tagTable), BorderLayout.CENTER);
055        if (presetHandler != null) {
056            presetListPanel = new PresetListPanel();
057            if (Config.getPref().getBoolean("relation.editor.presets.visible", true)) {
058                pnl.add(presetListPanel, BorderLayout.NORTH);
059            }
060        }
061        return pnl;
062    }
063
064    /**
065     * Sets the next component to request focus after navigation (with tab or enter).
066     * @param nextFocusComponent next component to request focus after navigation (with tab or enter)
067     * @see TagTable#setNextFocusComponent
068     */
069    public void setNextFocusComponent(Component nextFocusComponent) {
070        tagTable.setNextFocusComponent(nextFocusComponent);
071    }
072
073    /**
074     * builds the panel with the button row
075     *
076     * @return the panel
077     */
078    protected JPanel buildButtonsPanel() {
079        JPanel pnl = new JPanel();
080        pnl.setLayout(new BoxLayout(pnl, BoxLayout.Y_AXIS));
081
082        buildButton(pnl, tagTable.getAddAction());
083        buildButton(pnl, tagTable.getDeleteAction());
084        buildButton(pnl, tagTable.getPasteAction());
085
086        return pnl;
087    }
088
089    private void buildButton(JPanel pnl, AbstractAction action) {
090        JButton btn = new JButton(action);
091        pnl.add(btn);
092        btn.setMargin(new Insets(0, 0, 0, 0));
093        tagTable.addComponentNotStoppingCellEditing(btn);
094    }
095
096    /**
097     * Returns the paste action.
098     * @return the paste action
099     */
100    public AbstractAction getPasteAction() {
101        return tagTable.getPasteAction();
102    }
103
104    /**
105     * builds the GUI
106     */
107    protected final void build() {
108        setLayout(new GridBagLayout());
109        JPanel tablePanel = buildTagTableEditorPanel();
110        JPanel buttonPanel = buildButtonsPanel();
111
112        GridBagConstraints gc = new GridBagConstraints();
113
114        // -- buttons panel
115        //
116        gc.fill = GridBagConstraints.VERTICAL;
117        gc.weightx = 0.0;
118        gc.weighty = 1.0;
119        gc.anchor = GridBagConstraints.NORTHWEST;
120        add(buttonPanel, gc);
121
122        // -- the panel with the editor table
123        //
124        gc.gridx = 1;
125        gc.fill = GridBagConstraints.BOTH;
126        gc.weightx = 1.0;
127        gc.weighty = 1.0;
128        gc.anchor = GridBagConstraints.CENTER;
129        add(tablePanel, gc);
130
131        if (presetHandler != null) {
132            model.addTableModelListener(e -> updatePresets());
133        }
134
135        addFocusListener(new FocusAdapter() {
136            @Override
137            public void focusGained(FocusEvent e) {
138                tagTable.requestFocusInCell(0, 0);
139            }
140        });
141    }
142
143    /**
144     * Creates a new tag editor panel. The editor model is created
145     * internally and can be retrieved with {@link #getModel()}.
146     * @param primitive primitive to consider
147     * @param presetHandler tagging preset handler
148     */
149    public TagEditorPanel(OsmPrimitive primitive, TaggingPresetHandler presetHandler) {
150        this(new TagEditorModel().forPrimitive(primitive), presetHandler, 0);
151    }
152
153    /**
154     * Creates a new tag editor panel with a supplied model. If {@code model} is null, a new model is created.
155     *
156     * @param model the tag editor model
157     * @param presetHandler tagging preset handler
158     * @param maxCharacters maximum number of characters allowed, 0 for unlimited
159     */
160    public TagEditorPanel(TagEditorModel model, TaggingPresetHandler presetHandler, final int maxCharacters) {
161        this.model = model;
162        this.presetHandler = presetHandler;
163        if (this.model == null) {
164            this.model = new TagEditorModel();
165        }
166        this.tagTable = new TagTable(this.model, maxCharacters);
167
168        setupKeyboardShortcuts();
169        build();
170    }
171
172    private void setupKeyboardShortcuts() {
173        // F1 button = custom help action
174        final HelpAction helpTagAction = new HelpTagAction(tagTable,
175                viewRow -> this.model.get(tagTable.convertRowIndexToModel(viewRow)).getName(),
176                viewRow -> Collections.singletonMap(this.model.get(tagTable.convertRowIndexToModel(viewRow)).getValue(), 1));
177        getInputMap(JComponent.WHEN_ANCESTOR_OF_FOCUSED_COMPONENT).put(HelpAction.getKeyStroke(), "onHelp");
178        getActionMap().put("onHelp", helpTagAction);
179    }
180
181    /**
182     * Replies the tag editor model used by this panel.
183     *
184     * @return the tag editor model used by this panel
185     */
186    public TagEditorModel getModel() {
187        return model;
188    }
189
190    /**
191     * Initializes the auto completion infrastructure used in this
192     * tag editor panel. {@code layer} is the data layer from whose data set
193     * tag values are proposed as auto completion items.
194     *
195     * @param layer the data layer. Must not be null.
196     * @throws IllegalArgumentException if {@code layer} is null
197     */
198    public void initAutoCompletion(OsmDataLayer layer) {
199        CheckParameterUtil.ensureParameterNotNull(layer, "layer");
200
201        AutoCompletionManager autocomplete = AutoCompletionManager.of(layer.data);
202        AutoCompletionList acList = new AutoCompletionList();
203
204        TagCellEditor editor = (TagCellEditor) tagTable.getColumnModel().getColumn(0).getCellEditor();
205        editor.setAutoCompletionManager(autocomplete);
206        editor.setAutoCompletionList(acList);
207        editor = (TagCellEditor) tagTable.getColumnModel().getColumn(1).getCellEditor();
208        editor.setAutoCompletionManager(autocomplete);
209        editor.setAutoCompletionList(acList);
210    }
211
212    @Override
213    public void setEnabled(boolean enabled) {
214        tagTable.setEnabled(enabled);
215        super.setEnabled(enabled);
216    }
217
218    private void updatePresets() {
219        presetListPanel.updatePresets(
220                model.getTaggingPresetTypes(),
221                model.getTags(), presetHandler);
222        validate();
223    }
224
225    /**
226     * Save all outstanding edits to the model.
227     * @see org.openstreetmap.josm.gui.io.UploadDialog#saveEdits
228     * @since 18173
229     */
230    public void saveEdits() {
231        tagTable.endCellEditing();
232    }
233}