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}