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