001// License: GPL. For details, see LICENSE file.
002package org.openstreetmap.josm.gui.io;
003
004import static org.openstreetmap.josm.tools.I18n.tr;
005
006import java.awt.Component;
007import java.awt.GridBagLayout;
008import java.awt.event.ActionEvent;
009import java.awt.event.ActionListener;
010import java.awt.event.FocusEvent;
011import java.awt.event.FocusListener;
012import java.awt.event.ItemEvent;
013import java.awt.event.ItemListener;
014import java.awt.event.KeyEvent;
015import java.awt.event.KeyListener;
016import java.util.ArrayList;
017import java.util.Arrays;
018import java.util.Collection;
019import java.util.List;
020import java.util.Map;
021import java.util.Optional;
022import java.util.concurrent.TimeUnit;
023
024import javax.swing.BorderFactory;
025import javax.swing.JCheckBox;
026import javax.swing.JEditorPane;
027import javax.swing.JLabel;
028import javax.swing.JPanel;
029import javax.swing.JTextField;
030import javax.swing.event.HyperlinkEvent;
031import javax.swing.event.TableModelEvent;
032import javax.swing.event.TableModelListener;
033
034import org.openstreetmap.josm.data.osm.Changeset;
035import org.openstreetmap.josm.data.osm.OsmPrimitive;
036import org.openstreetmap.josm.gui.MainApplication;
037import org.openstreetmap.josm.gui.io.UploadTextComponentValidator.UploadAreaValidator;
038import org.openstreetmap.josm.gui.io.UploadTextComponentValidator.UploadCommentValidator;
039import org.openstreetmap.josm.gui.io.UploadTextComponentValidator.UploadSourceValidator;
040import org.openstreetmap.josm.gui.tagging.TagModel;
041import org.openstreetmap.josm.gui.widgets.HistoryComboBox;
042import org.openstreetmap.josm.gui.widgets.JMultilineLabel;
043import org.openstreetmap.josm.spi.preferences.Config;
044import org.openstreetmap.josm.tools.GBC;
045import org.openstreetmap.josm.tools.Utils;
046
047/**
048 * BasicUploadSettingsPanel allows to enter the basic parameters required for uploading data.
049 * @since 2599
050 */
051public class BasicUploadSettingsPanel extends JPanel implements ActionListener, FocusListener, ItemListener, KeyListener, TableModelListener {
052    /**
053     * Preference name for the history of comments
054     */
055    public static final String COMMENT_HISTORY_KEY = "upload.comment.history";
056    /**
057     * Preference name for last used upload comment
058     */
059    public static final String COMMENT_LAST_USED_KEY = "upload.comment.last-used";
060    /**
061     * Preference name for the max age search comments may have
062     */
063    public static final String COMMENT_MAX_AGE_KEY = "upload.comment.max-age";
064    /**
065     * Preference name for the history of sources
066     */
067    public static final String SOURCE_HISTORY_KEY = "upload.source.history";
068
069    /** the history combo box for the upload comment */
070    private final HistoryComboBox hcbUploadComment = new HistoryComboBox();
071    private final HistoryComboBox hcbUploadSource = new HistoryComboBox();
072    private final transient JCheckBox obtainSourceAutomatically = new JCheckBox(
073            tr("Automatically obtain source from current layers"));
074    /** the panel with a summary of the upload parameters */
075    private final UploadParameterSummaryPanel pnlUploadParameterSummary = new UploadParameterSummaryPanel();
076    /** the checkbox to request feedback from other users */
077    private final JCheckBox cbRequestReview = new JCheckBox(tr("I would like someone to review my edits."));
078    private final JLabel areaValidatorFeedback = new JLabel();
079    private final UploadAreaValidator areaValidator = new UploadAreaValidator(new JTextField(), areaValidatorFeedback);
080    /** the changeset comment model */
081    private final transient UploadDialogModel model;
082    private final transient JLabel uploadCommentFeedback = new JLabel();
083    private final transient UploadCommentValidator uploadCommentValidator = new UploadCommentValidator(
084            hcbUploadComment.getEditorComponent(), uploadCommentFeedback);
085    private final transient JLabel hcbUploadSourceFeedback = new JLabel();
086    private final transient UploadSourceValidator uploadSourceValidator = new UploadSourceValidator(
087            hcbUploadSource.getEditorComponent(), hcbUploadSourceFeedback);
088
089    /** a lock to prevent loops in notifications */
090    private boolean locked;
091
092    /**
093     * Creates the panel
094     *
095     * @param model The tag editor model.
096     *
097     * @since 18173 (signature)
098     */
099    public BasicUploadSettingsPanel(UploadDialogModel model) {
100        this.model = model;
101        this.model.addTableModelListener(this);
102        build();
103    }
104
105    protected void build() {
106        setLayout(new GridBagLayout());
107        setBorder(BorderFactory.createEmptyBorder(3, 3, 3, 3));
108        GBC gbc = GBC.eop().fill(GBC.HORIZONTAL);
109        add(buildUploadCommentPanel(), gbc);
110        add(buildUploadSourcePanel(), gbc);
111        add(pnlUploadParameterSummary, gbc);
112        if (Config.getPref().getBoolean("upload.show.review.request", true)) {
113            add(cbRequestReview, gbc);
114            cbRequestReview.addItemListener(this);
115        }
116        add(areaValidatorFeedback, gbc);
117        add(new JPanel(), GBC.std().fill(GBC.BOTH));
118    }
119
120    protected JPanel buildUploadCommentPanel() {
121        JPanel pnl = new JPanel(new GridBagLayout());
122        pnl.setBorder(BorderFactory.createTitledBorder(tr("Provide a brief comment for the changes you are uploading:")));
123
124        hcbUploadComment.setToolTipText(tr("Enter an upload comment"));
125        hcbUploadComment.getEditorComponent().setMaxTextLength(Changeset.MAX_CHANGESET_TAG_LENGTH);
126        JTextField editor = hcbUploadComment.getEditorComponent();
127        editor.getDocument().putProperty("tag", "comment");
128        editor.addKeyListener(this);
129        editor.addFocusListener(this);
130        editor.addActionListener(this);
131        GBC gbc = GBC.eol().insets(3).fill(GBC.HORIZONTAL);
132        pnl.add(hcbUploadComment, gbc);
133        pnl.add(uploadCommentFeedback, gbc);
134        return pnl;
135    }
136
137    protected JPanel buildUploadSourcePanel() {
138        JPanel pnl = new JPanel(new GridBagLayout());
139        pnl.setBorder(BorderFactory.createTitledBorder(tr("Specify the data source for the changes")));
140
141        JEditorPane obtainSourceOnce = new JMultilineLabel(
142                "<html>(<a href=\"urn:changeset-source\">" + tr("just once") + "</a>)</html>");
143        obtainSourceOnce.addHyperlinkListener(e -> {
144            if (HyperlinkEvent.EventType.ACTIVATED.equals(e.getEventType())) {
145                saveEdits();
146                model.put("source", getSourceFromLayer());
147            }
148        });
149        obtainSourceAutomatically.setSelected(Config.getPref().getBoolean("upload.source.obtainautomatically", false));
150        obtainSourceAutomatically.addActionListener(e -> {
151            if (obtainSourceAutomatically.isSelected()) {
152                model.put("source", getSourceFromLayer());
153            }
154            obtainSourceOnce.setVisible(!obtainSourceAutomatically.isSelected());
155        });
156        JPanel obtainSource = new JPanel(new GridBagLayout());
157        obtainSource.add(obtainSourceAutomatically, GBC.std().anchor(GBC.WEST));
158        obtainSource.add(obtainSourceOnce, GBC.std().anchor(GBC.WEST));
159        obtainSource.add(new JLabel(), GBC.eol().fill(GBC.HORIZONTAL));
160
161        hcbUploadSource.setToolTipText(tr("Enter a source"));
162        hcbUploadSource.getEditorComponent().setMaxTextLength(Changeset.MAX_CHANGESET_TAG_LENGTH);
163        JTextField editor = hcbUploadSource.getEditorComponent();
164        editor.getDocument().putProperty("tag", "source");
165        editor.addKeyListener(this);
166        editor.addFocusListener(this);
167        editor.addActionListener(this);
168        GBC gbc = GBC.eol().insets(3).fill(GBC.HORIZONTAL);
169        if (Config.getPref().getBoolean("upload.show.automatic.source", true)) {
170            pnl.add(obtainSource, gbc);
171        }
172        pnl.add(hcbUploadSource, gbc);
173        pnl.add(hcbUploadSourceFeedback, gbc);
174        return pnl;
175    }
176
177    /**
178     * Initializes this life cycle of the panel.
179     *
180     * Adds the comment and source tags from history, and/or obtains the source from the layer if
181     * the user said so.
182     *
183     * @param map Map where tags are added to.
184     * @since 18173
185     */
186    public void initLifeCycle(Map<String, String> map) {
187        Optional.ofNullable(getLastChangesetTagFromHistory(COMMENT_HISTORY_KEY, new ArrayList<>())).ifPresent(
188                x -> map.put("comment", x));
189        Optional.ofNullable(getLastChangesetTagFromHistory(SOURCE_HISTORY_KEY, getDefaultSources())).ifPresent(
190                x -> map.put("source", x));
191        if (obtainSourceAutomatically.isSelected()) {
192            map.put("source", getSourceFromLayer());
193        }
194        hcbUploadComment.getModel().prefs().load(COMMENT_HISTORY_KEY);
195        hcbUploadComment.discardAllUndoableEdits();
196        hcbUploadSource.getModel().prefs().load(SOURCE_HISTORY_KEY, getDefaultSources());
197        hcbUploadSource.discardAllUndoableEdits();
198        hcbUploadComment.getEditorComponent().requestFocusInWindow();
199        uploadCommentValidator.validate();
200        uploadSourceValidator.validate();
201    }
202
203    /**
204     * Get a key's value from the model.
205     * @param key The key
206     * @return The value or ""
207     * @since 18173
208     */
209    private String get(String key) {
210        TagModel tm = model.get(key);
211        return tm == null ? "" : tm.getValue();
212    }
213
214    /**
215     * Get the topmost item from the history if not expired.
216     *
217     * @param historyKey The preferences key.
218     * @param def A default history.
219     * @return The history item (may be null).
220     * @since 18173 (signature)
221     */
222    public static String getLastChangesetTagFromHistory(String historyKey, List<String> def) {
223        Collection<String> history = Config.getPref().getList(historyKey, def);
224        long age = TimeUnit.MILLISECONDS.toSeconds(System.currentTimeMillis()) - getHistoryLastUsedKey();
225        if (age < getHistoryMaxAgeKey() && !history.isEmpty()) {
226            return history.iterator().next();
227        }
228        return null;
229    }
230
231    /**
232     * Add the "source" tag
233     * @return The source from the layer info.
234     */
235    private String getSourceFromLayer() {
236        String source = MainApplication.getMap().mapView.getLayerInformationForSourceTag();
237        return Utils.shortenString(source, Changeset.MAX_CHANGESET_TAG_LENGTH);
238    }
239
240    /**
241     * Returns the default list of sources.
242     * @return the default list of sources
243     */
244    public static List<String> getDefaultSources() {
245        return Arrays.asList("knowledge", "survey", "Bing");
246    }
247
248    /**
249     * Returns the list of {@link UploadTextComponentValidator} defined by this panel.
250     * @return the list of {@code UploadTextComponentValidator} defined by this panel.
251     * @since 17238
252     */
253    protected List<UploadTextComponentValidator> getUploadTextValidators() {
254        return Arrays.asList(areaValidator, uploadCommentValidator, uploadSourceValidator);
255    }
256
257    /**
258     * Remembers the user input in the preference settings
259     */
260    public void rememberUserInput() {
261        // store the history of comments
262        if (getHistoryMaxAgeKey() > 0) {
263            hcbUploadComment.addCurrentItemToHistory();
264            hcbUploadComment.getModel().prefs().save(COMMENT_HISTORY_KEY);
265            Config.getPref().putLong(COMMENT_LAST_USED_KEY, TimeUnit.MILLISECONDS.toSeconds(System.currentTimeMillis()));
266        }
267        // store the history of sources
268        hcbUploadSource.addCurrentItemToHistory();
269        hcbUploadSource.getModel().prefs().save(SOURCE_HISTORY_KEY);
270
271        // store current value of obtaining source automatically
272        Config.getPref().putBoolean("upload.source.obtainautomatically", obtainSourceAutomatically.isSelected());
273    }
274
275    /**
276     * Initializes editing of upload comment.
277     */
278    public void initEditingOfUploadComment() {
279        hcbUploadComment.getEditor().selectAll();
280        hcbUploadComment.requestFocusInWindow();
281    }
282
283    /**
284     * Initializes editing of upload source.
285     */
286    public void initEditingOfUploadSource() {
287        hcbUploadSource.getEditor().selectAll();
288        hcbUploadSource.requestFocusInWindow();
289    }
290
291    void setUploadedPrimitives(List<OsmPrimitive> primitives) {
292        areaValidator.computeArea(primitives);
293    }
294
295    /**
296     * Returns the panel that displays a summary of data the user is about to upload.
297     * @return the upload parameter summary panel
298     */
299    public UploadParameterSummaryPanel getUploadParameterSummaryPanel() {
300        return pnlUploadParameterSummary;
301    }
302
303    static long getHistoryMaxAgeKey() {
304        return Config.getPref().getLong(COMMENT_MAX_AGE_KEY, TimeUnit.HOURS.toSeconds(4));
305    }
306
307    static long getHistoryLastUsedKey() {
308        return Config.getPref().getLong(COMMENT_LAST_USED_KEY, 0);
309    }
310
311    /**
312     * Updates the combobox histories when a combobox editor loses focus.
313     *
314     * @param text The {@code JTextField} of the combobox editor.
315     */
316    private void updateHistory(JTextField text) {
317        String tag = (String) text.getDocument().getProperty("tag"); // tag is either "comment" or "source"
318        if ("comment".equals(tag)) {
319            hcbUploadComment.addCurrentItemToHistory();
320        } else if ("source".equals(tag)) {
321            hcbUploadSource.addCurrentItemToHistory();
322        }
323    }
324
325    /**
326     * Updates the table editor model with changes in the comboboxes.
327     *
328     * The lock prevents loops in change notifications, eg. the combobox
329     * notifies the table model and the table model notifies the combobox, which
330     * throws IllegalStateException.
331     *
332     * @param text The {@code JTextField} of the combobox editor.
333     */
334    private void updateModel(JTextField text) {
335        if (!locked) {
336            locked = true;
337            try {
338                String tag = (String) text.getDocument().getProperty("tag"); // tag is either "comment" or "source"
339                String value = text.getText();
340                model.put(tag, value.isEmpty() ? null : value); // remove tags with empty values
341            } finally {
342                locked = false;
343            }
344        }
345    }
346
347    /**
348     * Save all outstanding edits to the model.
349     * @see UploadDialog#saveEdits
350     * @since 18173
351     */
352    public void saveEdits() {
353        updateModel(hcbUploadComment.getEditorComponent());
354        hcbUploadComment.addCurrentItemToHistory();
355        updateModel(hcbUploadSource.getEditorComponent());
356        hcbUploadSource.addCurrentItemToHistory();
357    }
358
359    /**
360     * Returns the UplodDialog that is our ancestor
361     *
362     * @return the UploadDialog or null
363     */
364    private UploadDialog getDialog() {
365        Component d = getRootPane();
366        while ((d = d.getParent()) != null) {
367            if (d instanceof UploadDialog)
368                return (UploadDialog) d;
369        }
370        return null;
371    }
372
373    /**
374     * Update the model when the selection changes in a combobox.
375     * @param e The action event.
376     */
377    @Override
378    public void actionPerformed(ActionEvent e) {
379        setFocusToUploadButton();
380    }
381
382    @Override
383    public void focusGained(FocusEvent e) {
384    }
385
386    /**
387     * Update the model and combobox history when a combobox editor loses focus.
388     */
389    @Override
390    public void focusLost(FocusEvent e) {
391        Object c = e.getSource();
392        if (c instanceof JTextField) {
393            updateModel((JTextField) c);
394            updateHistory((JTextField) c);
395        }
396    }
397
398    /**
399     * Updates the table editor model upon changes in the "review" checkbox.
400     */
401    @Override
402    public void itemStateChanged(ItemEvent e) {
403        if (!locked) {
404            locked = true;
405            try {
406                model.put("review_requested", e.getStateChange() == ItemEvent.SELECTED ? "yes" : null);
407            } finally {
408                locked = false;
409            }
410        }
411    }
412
413    /**
414     * Updates the controls upon changes in the table editor model.
415     */
416    @Override
417    public void tableChanged(TableModelEvent e) {
418        if (!locked) {
419            locked = true;
420            try {
421                hcbUploadComment.setText(get("comment"));
422                hcbUploadSource.setText(get("source"));
423                cbRequestReview.setSelected(get("review_requested").equals("yes"));
424            } finally {
425                locked = false;
426            }
427        }
428    }
429
430    /**
431     * Set the focus directly to the upload button if "Enter" key is pressed in any combobox.
432     */
433    @Override
434    public void keyTyped(KeyEvent e) {
435        if (e.getKeyChar() == KeyEvent.VK_ENTER) {
436            setFocusToUploadButton();
437        }
438    }
439
440    @Override
441    public void keyPressed(KeyEvent e) {
442    }
443
444    @Override
445    public void keyReleased(KeyEvent e) {
446    }
447
448    private void setFocusToUploadButton() {
449        Optional.ofNullable(getDialog()).ifPresent(UploadDialog::setFocusToUploadButton);
450    }
451}