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.BorderLayout;
007import java.awt.GridBagLayout;
008import java.awt.event.ActionEvent;
009import java.awt.event.ActionListener;
010import java.awt.event.FocusAdapter;
011import java.awt.event.FocusEvent;
012import java.awt.event.ItemEvent;
013import java.awt.event.KeyAdapter;
014import java.awt.event.KeyEvent;
015import java.util.Arrays;
016import java.util.LinkedList;
017import java.util.List;
018import java.util.Objects;
019import java.util.concurrent.TimeUnit;
020
021import javax.swing.BorderFactory;
022import javax.swing.JCheckBox;
023import javax.swing.JEditorPane;
024import javax.swing.JLabel;
025import javax.swing.JPanel;
026import javax.swing.event.AncestorEvent;
027import javax.swing.event.AncestorListener;
028import javax.swing.event.ChangeEvent;
029import javax.swing.event.ChangeListener;
030import javax.swing.event.HyperlinkEvent;
031
032import org.openstreetmap.josm.data.osm.Changeset;
033import org.openstreetmap.josm.gui.MainApplication;
034import org.openstreetmap.josm.gui.widgets.HistoryComboBox;
035import org.openstreetmap.josm.gui.widgets.JMultilineLabel;
036import org.openstreetmap.josm.spi.preferences.Config;
037import org.openstreetmap.josm.tools.GBC;
038import org.openstreetmap.josm.tools.Utils;
039
040/**
041 * BasicUploadSettingsPanel allows to enter the basic parameters required for uploading data.
042 * @since 2599
043 */
044public class BasicUploadSettingsPanel extends JPanel {
045    /**
046     * Preference name for history collection
047     */
048    public static final String HISTORY_KEY = "upload.comment.history";
049    /**
050     * Preference name for last used upload comment
051     */
052    public static final String HISTORY_LAST_USED_KEY = "upload.comment.last-used";
053    /**
054     * Preference name for the max age search comments may have
055     */
056    public static final String HISTORY_MAX_AGE_KEY = "upload.comment.max-age";
057    /**
058     * Preference name for the history of source values
059     */
060    public static final String SOURCE_HISTORY_KEY = "upload.source.history";
061
062    /** the history combo box for the upload comment */
063    private final HistoryComboBox hcbUploadComment = new HistoryComboBox();
064    private final HistoryComboBox hcbUploadSource = new HistoryComboBox();
065    private final transient JCheckBox obtainSourceAutomatically = new JCheckBox(
066            tr("Automatically obtain source from current layers"));
067    /** the panel with a summary of the upload parameters */
068    private final UploadParameterSummaryPanel pnlUploadParameterSummary = new UploadParameterSummaryPanel();
069    /** the checkbox to request feedback from other users */
070    private final JCheckBox cbRequestReview = new JCheckBox(tr("I would like someone to review my edits."));
071    /** the changeset comment model */
072    private final transient ChangesetCommentModel changesetCommentModel;
073    private final transient ChangesetCommentModel changesetSourceModel;
074    private final transient ChangesetReviewModel changesetReviewModel;
075
076    protected JPanel buildUploadCommentPanel() {
077        JPanel pnl = new JPanel(new GridBagLayout());
078
079        JEditorPane commentLabel = new JMultilineLabel("<html><b>" + tr("Provide a brief comment for the changes you are uploading:"));
080        pnl.add(commentLabel, GBC.eol().insets(0, 5, 10, 3).fill(GBC.HORIZONTAL));
081        hcbUploadComment.setToolTipText(tr("Enter an upload comment"));
082        hcbUploadComment.setMaxTextLength(Changeset.MAX_CHANGESET_TAG_LENGTH);
083        populateHistoryComboBox(hcbUploadComment, HISTORY_KEY, new LinkedList<String>());
084        CommentModelListener commentModelListener = new CommentModelListener(hcbUploadComment, changesetCommentModel);
085        hcbUploadComment.getEditor().addActionListener(commentModelListener);
086        hcbUploadComment.getEditorComponent().addFocusListener(commentModelListener);
087        pnl.add(hcbUploadComment, GBC.eol().fill(GBC.HORIZONTAL));
088
089        JEditorPane sourceLabel = new JMultilineLabel("<html><b>" + tr("Specify the data source for the changes") + ":</b>");
090        pnl.add(sourceLabel, GBC.eol().insets(0, 8, 10, 0).fill(GBC.HORIZONTAL));
091        JEditorPane obtainSourceOnce = new JMultilineLabel(
092                "<html>(<a href=\"urn:changeset-source\">" + tr("just once") + "</a>)</html>");
093        obtainSourceOnce.addHyperlinkListener(e -> {
094            if (HyperlinkEvent.EventType.ACTIVATED.equals(e.getEventType())) {
095                automaticallyAddSource();
096            }
097        });
098        obtainSourceAutomatically.setSelected(Config.getPref().getBoolean("upload.source.obtainautomatically", false));
099        obtainSourceAutomatically.addActionListener(e -> {
100            if (obtainSourceAutomatically.isSelected())
101                automaticallyAddSource();
102
103            obtainSourceOnce.setVisible(!obtainSourceAutomatically.isSelected());
104        });
105        JPanel obtainSource = new JPanel(new GridBagLayout());
106        obtainSource.add(obtainSourceAutomatically, GBC.std().anchor(GBC.WEST));
107        obtainSource.add(obtainSourceOnce, GBC.std().anchor(GBC.WEST));
108        obtainSource.add(new JLabel(), GBC.eol().fill(GBC.HORIZONTAL));
109        pnl.add(obtainSource, GBC.eol().insets(0, 0, 10, 3).fill(GBC.HORIZONTAL));
110
111        hcbUploadSource.setToolTipText(tr("Enter a source"));
112        hcbUploadSource.setMaxTextLength(Changeset.MAX_CHANGESET_TAG_LENGTH);
113        populateHistoryComboBox(hcbUploadSource, SOURCE_HISTORY_KEY, getDefaultSources());
114        CommentModelListener sourceModelListener = new CommentModelListener(hcbUploadSource, changesetSourceModel);
115        hcbUploadSource.getEditor().addActionListener(sourceModelListener);
116        hcbUploadSource.getEditorComponent().addFocusListener(sourceModelListener);
117        pnl.add(hcbUploadSource, GBC.eol().fill(GBC.HORIZONTAL));
118        if (obtainSourceAutomatically.isSelected()) {
119            automaticallyAddSource();
120        }
121        pnl.addAncestorListener(new AncestorListener() {
122            @Override
123            public void ancestorAdded(AncestorEvent event) {
124                if (obtainSourceAutomatically.isSelected())
125                    automaticallyAddSource();
126            }
127
128            @Override
129            public void ancestorRemoved(AncestorEvent event) {
130                // Do nothing
131            }
132
133            @Override
134            public void ancestorMoved(AncestorEvent event) {
135                // Do nothing
136            }
137        });
138        return pnl;
139    }
140
141    /**
142     * Add the source tags
143     */
144    protected void automaticallyAddSource() {
145        final String source = MainApplication.getMap().mapView.getLayerInformationForSourceTag();
146        hcbUploadSource.setText(Utils.shortenString(source, Changeset.MAX_CHANGESET_TAG_LENGTH));
147        changesetSourceModel.setComment(hcbUploadSource.getText()); // Fix #9965
148    }
149
150    /**
151     * Refreshes contents of upload history combo boxes from preferences.
152     */
153    protected void refreshHistoryComboBoxes() {
154        populateHistoryComboBox(hcbUploadComment, HISTORY_KEY, new LinkedList<String>());
155        populateHistoryComboBox(hcbUploadSource, SOURCE_HISTORY_KEY, getDefaultSources());
156    }
157
158    private static void populateHistoryComboBox(HistoryComboBox hcb, String historyKey, List<String> defaultValues) {
159        hcb.setPossibleItemsTopDown(Config.getPref().getList(historyKey, defaultValues));
160        hcb.discardAllUndoableEdits();
161    }
162
163    /**
164     * Discards undoable edits of upload history combo boxes.
165     */
166    protected void discardAllUndoableEdits() {
167        hcbUploadComment.discardAllUndoableEdits();
168        hcbUploadSource.discardAllUndoableEdits();
169    }
170
171    /**
172     * Returns the default list of sources.
173     * @return the default list of sources
174     */
175    public static List<String> getDefaultSources() {
176        return Arrays.asList("knowledge", "survey", "Bing");
177    }
178
179    protected void build() {
180        setLayout(new BorderLayout());
181        setBorder(BorderFactory.createEmptyBorder(3, 3, 3, 3));
182        add(buildUploadCommentPanel(), BorderLayout.NORTH);
183        add(pnlUploadParameterSummary, BorderLayout.CENTER);
184        add(cbRequestReview, BorderLayout.SOUTH);
185        cbRequestReview.addItemListener(e -> changesetReviewModel.setReviewRequested(e.getStateChange() == ItemEvent.SELECTED));
186    }
187
188    /**
189     * Creates the panel
190     *
191     * @param changesetCommentModel the model for the changeset comment. Must not be null
192     * @param changesetSourceModel the model for the changeset source. Must not be null.
193     * @param changesetReviewModel the model for the changeset review. Must not be null.
194     * @throws NullPointerException if a model is null
195     * @since 12719 (signature)
196     */
197    public BasicUploadSettingsPanel(ChangesetCommentModel changesetCommentModel, ChangesetCommentModel changesetSourceModel,
198            ChangesetReviewModel changesetReviewModel) {
199        this.changesetCommentModel = Objects.requireNonNull(changesetCommentModel, "changesetCommentModel");
200        this.changesetSourceModel = Objects.requireNonNull(changesetSourceModel, "changesetSourceModel");
201        this.changesetReviewModel = Objects.requireNonNull(changesetReviewModel, "changesetReviewModel");
202        changesetCommentModel.addChangeListener(new ChangesetCommentChangeListener(hcbUploadComment));
203        changesetSourceModel.addChangeListener(new ChangesetCommentChangeListener(hcbUploadSource));
204        changesetReviewModel.addChangeListener(new ChangesetReviewChangeListener());
205        build();
206    }
207
208    void setUploadTagDownFocusTraversalHandlers(final ActionListener handler) {
209        setHistoryComboBoxDownFocusTraversalHandler(handler, hcbUploadComment);
210        setHistoryComboBoxDownFocusTraversalHandler(handler, hcbUploadSource);
211    }
212
213    private static void setHistoryComboBoxDownFocusTraversalHandler(ActionListener handler, HistoryComboBox hcb) {
214        hcb.getEditor().addActionListener(handler);
215        hcb.getEditorComponent().addKeyListener(new HistoryComboBoxKeyAdapter(hcb, handler));
216    }
217
218    /**
219     * Remembers the user input in the preference settings
220     */
221    public void rememberUserInput() {
222        // store the history of comments
223        if (getHistoryMaxAgeKey() > 0) {
224            hcbUploadComment.addCurrentItemToHistory();
225            Config.getPref().putList(HISTORY_KEY, hcbUploadComment.getHistory());
226            Config.getPref().putLong(HISTORY_LAST_USED_KEY, TimeUnit.MILLISECONDS.toSeconds(System.currentTimeMillis()));
227        }
228        // store the history of sources
229        hcbUploadSource.addCurrentItemToHistory();
230        Config.getPref().putList(SOURCE_HISTORY_KEY, hcbUploadSource.getHistory());
231
232        // store current value of obtaining source automatically
233        Config.getPref().putBoolean("upload.source.obtainautomatically", obtainSourceAutomatically.isSelected());
234    }
235
236    /**
237     * Initializes the panel for user input
238     */
239    public void startUserInput() {
240        hcbUploadComment.requestFocusInWindow();
241        hcbUploadComment.getEditorComponent().requestFocusInWindow();
242    }
243
244    /**
245     * Initializes editing of upload comment.
246     */
247    public void initEditingOfUploadComment() {
248        hcbUploadComment.getEditor().selectAll();
249        hcbUploadComment.requestFocusInWindow();
250    }
251
252    /**
253     * Initializes editing of upload source.
254     */
255    public void initEditingOfUploadSource() {
256        hcbUploadSource.getEditor().selectAll();
257        hcbUploadSource.requestFocusInWindow();
258    }
259
260    /**
261     * Returns the panel that displays a summary of data the user is about to upload.
262     * @return the upload parameter summary panel
263     */
264    public UploadParameterSummaryPanel getUploadParameterSummaryPanel() {
265        return pnlUploadParameterSummary;
266    }
267
268    /**
269     * Forces update of comment/source model if matching text field is focused.
270     * @since 14977
271     */
272    public void forceUpdateActiveField() {
273        updateModelIfFocused(hcbUploadComment, changesetCommentModel);
274        updateModelIfFocused(hcbUploadSource, changesetSourceModel);
275    }
276
277    private static void updateModelIfFocused(HistoryComboBox hcb, ChangesetCommentModel changesetModel) {
278        if (hcb.getEditorComponent().hasFocus()) {
279            changesetModel.setComment(hcb.getText());
280        }
281    }
282
283    static long getHistoryMaxAgeKey() {
284        return Config.getPref().getLong(HISTORY_MAX_AGE_KEY, TimeUnit.HOURS.toSeconds(4));
285    }
286
287    static long getHistoryLastUsedKey() {
288        return Config.getPref().getLong(BasicUploadSettingsPanel.HISTORY_LAST_USED_KEY, 0);
289    }
290
291    static final class HistoryComboBoxKeyAdapter extends KeyAdapter {
292        private final HistoryComboBox hcb;
293        private final ActionListener handler;
294
295        HistoryComboBoxKeyAdapter(HistoryComboBox hcb, ActionListener handler) {
296            this.hcb = hcb;
297            this.handler = handler;
298        }
299
300        @Override
301        public void keyTyped(KeyEvent e) {
302            if (e.getKeyCode() == KeyEvent.VK_TAB) {
303                handler.actionPerformed(new ActionEvent(hcb, 0, "focusDown"));
304            }
305        }
306    }
307
308    /**
309     * Updates the changeset comment model upon changes in the input field.
310     */
311    static class CommentModelListener extends FocusAdapter implements ActionListener {
312
313        private final HistoryComboBox source;
314        private final ChangesetCommentModel destination;
315
316        CommentModelListener(HistoryComboBox source, ChangesetCommentModel destination) {
317            this.source = source;
318            this.destination = destination;
319        }
320
321        @Override
322        public void actionPerformed(ActionEvent e) {
323            destination.setComment(source.getText());
324        }
325
326        @Override
327        public void focusLost(FocusEvent e) {
328            destination.setComment(source.getText());
329        }
330    }
331
332    /**
333     * Observes the changeset comment model and keeps the comment input field
334     * in sync with the current changeset comment
335     */
336    static class ChangesetCommentChangeListener implements ChangeListener {
337
338        private final HistoryComboBox destination;
339
340        ChangesetCommentChangeListener(HistoryComboBox destination) {
341            this.destination = destination;
342        }
343
344        @Override
345        public void stateChanged(ChangeEvent e) {
346            if (!(e.getSource() instanceof ChangesetCommentModel)) return;
347            String newComment = ((ChangesetCommentModel) e.getSource()).getComment();
348            if (!destination.getText().equals(newComment)) {
349                destination.setText(newComment);
350            }
351        }
352    }
353
354    /**
355     * Observes the changeset review model and keeps the review checkbox
356     * in sync with the current changeset review request
357     */
358    class ChangesetReviewChangeListener implements ChangeListener {
359        @Override
360        public void stateChanged(ChangeEvent e) {
361            if (!(e.getSource() instanceof ChangesetReviewModel)) return;
362            boolean newState = ((ChangesetReviewModel) e.getSource()).isReviewRequested();
363            if (cbRequestReview.isSelected() != newState) {
364                cbRequestReview.setSelected(newState);
365            }
366        }
367    }
368}