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