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