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.Component; 007import java.awt.GridBagLayout; 008import java.awt.event.ActionEvent; 009import java.awt.event.ActionListener; 010import java.awt.event.FocusEvent; 011import java.awt.event.FocusListener; 012import java.awt.event.ItemEvent; 013import java.awt.event.ItemListener; 014import java.awt.event.KeyEvent; 015import java.awt.event.KeyListener; 016import java.util.ArrayList; 017import java.util.Arrays; 018import java.util.Collection; 019import java.util.List; 020import java.util.Map; 021import java.util.Optional; 022import java.util.concurrent.TimeUnit; 023 024import javax.swing.BorderFactory; 025import javax.swing.JCheckBox; 026import javax.swing.JEditorPane; 027import javax.swing.JLabel; 028import javax.swing.JPanel; 029import javax.swing.JTextField; 030import javax.swing.event.HyperlinkEvent; 031import javax.swing.event.TableModelEvent; 032import javax.swing.event.TableModelListener; 033 034import org.openstreetmap.josm.data.osm.Changeset; 035import org.openstreetmap.josm.data.osm.OsmPrimitive; 036import org.openstreetmap.josm.gui.MainApplication; 037import org.openstreetmap.josm.gui.io.UploadTextComponentValidator.UploadAreaValidator; 038import org.openstreetmap.josm.gui.io.UploadTextComponentValidator.UploadCommentValidator; 039import org.openstreetmap.josm.gui.io.UploadTextComponentValidator.UploadSourceValidator; 040import org.openstreetmap.josm.gui.tagging.TagModel; 041import org.openstreetmap.josm.gui.widgets.HistoryComboBox; 042import org.openstreetmap.josm.gui.widgets.JMultilineLabel; 043import org.openstreetmap.josm.spi.preferences.Config; 044import org.openstreetmap.josm.tools.GBC; 045import org.openstreetmap.josm.tools.Utils; 046 047/** 048 * BasicUploadSettingsPanel allows to enter the basic parameters required for uploading data. 049 * @since 2599 050 */ 051public class BasicUploadSettingsPanel extends JPanel implements ActionListener, FocusListener, ItemListener, KeyListener, TableModelListener { 052 /** 053 * Preference name for the history of comments 054 */ 055 public static final String COMMENT_HISTORY_KEY = "upload.comment.history"; 056 /** 057 * Preference name for last used upload comment 058 */ 059 public static final String COMMENT_LAST_USED_KEY = "upload.comment.last-used"; 060 /** 061 * Preference name for the max age search comments may have 062 */ 063 public static final String COMMENT_MAX_AGE_KEY = "upload.comment.max-age"; 064 /** 065 * Preference name for the history of sources 066 */ 067 public static final String SOURCE_HISTORY_KEY = "upload.source.history"; 068 069 /** the history combo box for the upload comment */ 070 private final HistoryComboBox hcbUploadComment = new HistoryComboBox(); 071 private final HistoryComboBox hcbUploadSource = new HistoryComboBox(); 072 private final transient JCheckBox obtainSourceAutomatically = new JCheckBox( 073 tr("Automatically obtain source from current layers")); 074 /** the panel with a summary of the upload parameters */ 075 private final UploadParameterSummaryPanel pnlUploadParameterSummary = new UploadParameterSummaryPanel(); 076 /** the checkbox to request feedback from other users */ 077 private final JCheckBox cbRequestReview = new JCheckBox(tr("I would like someone to review my edits.")); 078 private final JLabel areaValidatorFeedback = new JLabel(); 079 private final UploadAreaValidator areaValidator = new UploadAreaValidator(new JTextField(), areaValidatorFeedback); 080 /** the changeset comment model */ 081 private final transient UploadDialogModel model; 082 private final transient JLabel uploadCommentFeedback = new JLabel(); 083 private final transient UploadCommentValidator uploadCommentValidator = new UploadCommentValidator( 084 hcbUploadComment.getEditorComponent(), uploadCommentFeedback); 085 private final transient JLabel hcbUploadSourceFeedback = new JLabel(); 086 private final transient UploadSourceValidator uploadSourceValidator = new UploadSourceValidator( 087 hcbUploadSource.getEditorComponent(), hcbUploadSourceFeedback); 088 089 /** a lock to prevent loops in notifications */ 090 private boolean locked; 091 092 /** 093 * Creates the panel 094 * 095 * @param model The tag editor model. 096 * 097 * @since 18173 (signature) 098 */ 099 public BasicUploadSettingsPanel(UploadDialogModel model) { 100 this.model = model; 101 this.model.addTableModelListener(this); 102 build(); 103 } 104 105 protected void build() { 106 setLayout(new GridBagLayout()); 107 setBorder(BorderFactory.createEmptyBorder(3, 3, 3, 3)); 108 GBC gbc = GBC.eop().fill(GBC.HORIZONTAL); 109 add(buildUploadCommentPanel(), gbc); 110 add(buildUploadSourcePanel(), gbc); 111 add(pnlUploadParameterSummary, gbc); 112 if (Config.getPref().getBoolean("upload.show.review.request", true)) { 113 add(cbRequestReview, gbc); 114 cbRequestReview.addItemListener(this); 115 } 116 add(areaValidatorFeedback, gbc); 117 add(new JPanel(), GBC.std().fill(GBC.BOTH)); 118 } 119 120 protected JPanel buildUploadCommentPanel() { 121 JPanel pnl = new JPanel(new GridBagLayout()); 122 pnl.setBorder(BorderFactory.createTitledBorder(tr("Provide a brief comment for the changes you are uploading:"))); 123 124 hcbUploadComment.setToolTipText(tr("Enter an upload comment")); 125 hcbUploadComment.getEditorComponent().setMaxTextLength(Changeset.MAX_CHANGESET_TAG_LENGTH); 126 JTextField editor = hcbUploadComment.getEditorComponent(); 127 editor.getDocument().putProperty("tag", "comment"); 128 editor.addKeyListener(this); 129 editor.addFocusListener(this); 130 editor.addActionListener(this); 131 GBC gbc = GBC.eol().insets(3).fill(GBC.HORIZONTAL); 132 pnl.add(hcbUploadComment, gbc); 133 pnl.add(uploadCommentFeedback, gbc); 134 return pnl; 135 } 136 137 protected JPanel buildUploadSourcePanel() { 138 JPanel pnl = new JPanel(new GridBagLayout()); 139 pnl.setBorder(BorderFactory.createTitledBorder(tr("Specify the data source for the changes"))); 140 141 JEditorPane obtainSourceOnce = new JMultilineLabel( 142 "<html>(<a href=\"urn:changeset-source\">" + tr("just once") + "</a>)</html>"); 143 obtainSourceOnce.addHyperlinkListener(e -> { 144 if (HyperlinkEvent.EventType.ACTIVATED.equals(e.getEventType())) { 145 saveEdits(); 146 model.put("source", getSourceFromLayer()); 147 } 148 }); 149 obtainSourceAutomatically.setSelected(Config.getPref().getBoolean("upload.source.obtainautomatically", false)); 150 obtainSourceAutomatically.addActionListener(e -> { 151 if (obtainSourceAutomatically.isSelected()) { 152 model.put("source", getSourceFromLayer()); 153 } 154 obtainSourceOnce.setVisible(!obtainSourceAutomatically.isSelected()); 155 }); 156 JPanel obtainSource = new JPanel(new GridBagLayout()); 157 obtainSource.add(obtainSourceAutomatically, GBC.std().anchor(GBC.WEST)); 158 obtainSource.add(obtainSourceOnce, GBC.std().anchor(GBC.WEST)); 159 obtainSource.add(new JLabel(), GBC.eol().fill(GBC.HORIZONTAL)); 160 161 hcbUploadSource.setToolTipText(tr("Enter a source")); 162 hcbUploadSource.getEditorComponent().setMaxTextLength(Changeset.MAX_CHANGESET_TAG_LENGTH); 163 JTextField editor = hcbUploadSource.getEditorComponent(); 164 editor.getDocument().putProperty("tag", "source"); 165 editor.addKeyListener(this); 166 editor.addFocusListener(this); 167 editor.addActionListener(this); 168 GBC gbc = GBC.eol().insets(3).fill(GBC.HORIZONTAL); 169 if (Config.getPref().getBoolean("upload.show.automatic.source", true)) { 170 pnl.add(obtainSource, gbc); 171 } 172 pnl.add(hcbUploadSource, gbc); 173 pnl.add(hcbUploadSourceFeedback, gbc); 174 return pnl; 175 } 176 177 /** 178 * Initializes this life cycle of the panel. 179 * 180 * Adds the comment and source tags from history, and/or obtains the source from the layer if 181 * the user said so. 182 * 183 * @param map Map where tags are added to. 184 * @since 18173 185 */ 186 public void initLifeCycle(Map<String, String> map) { 187 Optional.ofNullable(getLastChangesetTagFromHistory(COMMENT_HISTORY_KEY, new ArrayList<>())).ifPresent( 188 x -> map.put("comment", x)); 189 Optional.ofNullable(getLastChangesetTagFromHistory(SOURCE_HISTORY_KEY, getDefaultSources())).ifPresent( 190 x -> map.put("source", x)); 191 if (obtainSourceAutomatically.isSelected()) { 192 map.put("source", getSourceFromLayer()); 193 } 194 hcbUploadComment.getModel().prefs().load(COMMENT_HISTORY_KEY); 195 hcbUploadComment.discardAllUndoableEdits(); 196 hcbUploadSource.getModel().prefs().load(SOURCE_HISTORY_KEY, getDefaultSources()); 197 hcbUploadSource.discardAllUndoableEdits(); 198 hcbUploadComment.getEditorComponent().requestFocusInWindow(); 199 uploadCommentValidator.validate(); 200 uploadSourceValidator.validate(); 201 } 202 203 /** 204 * Get a key's value from the model. 205 * @param key The key 206 * @return The value or "" 207 * @since 18173 208 */ 209 private String get(String key) { 210 TagModel tm = model.get(key); 211 return tm == null ? "" : tm.getValue(); 212 } 213 214 /** 215 * Get the topmost item from the history if not expired. 216 * 217 * @param historyKey The preferences key. 218 * @param def A default history. 219 * @return The history item (may be null). 220 * @since 18173 (signature) 221 */ 222 public static String getLastChangesetTagFromHistory(String historyKey, List<String> def) { 223 Collection<String> history = Config.getPref().getList(historyKey, def); 224 long age = TimeUnit.MILLISECONDS.toSeconds(System.currentTimeMillis()) - getHistoryLastUsedKey(); 225 if (age < getHistoryMaxAgeKey() && !history.isEmpty()) { 226 return history.iterator().next(); 227 } 228 return null; 229 } 230 231 /** 232 * Add the "source" tag 233 * @return The source from the layer info. 234 */ 235 private String getSourceFromLayer() { 236 String source = MainApplication.getMap().mapView.getLayerInformationForSourceTag(); 237 return Utils.shortenString(source, Changeset.MAX_CHANGESET_TAG_LENGTH); 238 } 239 240 /** 241 * Returns the default list of sources. 242 * @return the default list of sources 243 */ 244 public static List<String> getDefaultSources() { 245 return Arrays.asList("knowledge", "survey", "Bing"); 246 } 247 248 /** 249 * Returns the list of {@link UploadTextComponentValidator} defined by this panel. 250 * @return the list of {@code UploadTextComponentValidator} defined by this panel. 251 * @since 17238 252 */ 253 protected List<UploadTextComponentValidator> getUploadTextValidators() { 254 return Arrays.asList(areaValidator, uploadCommentValidator, uploadSourceValidator); 255 } 256 257 /** 258 * Remembers the user input in the preference settings 259 */ 260 public void rememberUserInput() { 261 // store the history of comments 262 if (getHistoryMaxAgeKey() > 0) { 263 hcbUploadComment.addCurrentItemToHistory(); 264 hcbUploadComment.getModel().prefs().save(COMMENT_HISTORY_KEY); 265 Config.getPref().putLong(COMMENT_LAST_USED_KEY, TimeUnit.MILLISECONDS.toSeconds(System.currentTimeMillis())); 266 } 267 // store the history of sources 268 hcbUploadSource.addCurrentItemToHistory(); 269 hcbUploadSource.getModel().prefs().save(SOURCE_HISTORY_KEY); 270 271 // store current value of obtaining source automatically 272 Config.getPref().putBoolean("upload.source.obtainautomatically", obtainSourceAutomatically.isSelected()); 273 } 274 275 /** 276 * Initializes editing of upload comment. 277 */ 278 public void initEditingOfUploadComment() { 279 hcbUploadComment.getEditor().selectAll(); 280 hcbUploadComment.requestFocusInWindow(); 281 } 282 283 /** 284 * Initializes editing of upload source. 285 */ 286 public void initEditingOfUploadSource() { 287 hcbUploadSource.getEditor().selectAll(); 288 hcbUploadSource.requestFocusInWindow(); 289 } 290 291 void setUploadedPrimitives(List<OsmPrimitive> primitives) { 292 areaValidator.computeArea(primitives); 293 } 294 295 /** 296 * Returns the panel that displays a summary of data the user is about to upload. 297 * @return the upload parameter summary panel 298 */ 299 public UploadParameterSummaryPanel getUploadParameterSummaryPanel() { 300 return pnlUploadParameterSummary; 301 } 302 303 static long getHistoryMaxAgeKey() { 304 return Config.getPref().getLong(COMMENT_MAX_AGE_KEY, TimeUnit.HOURS.toSeconds(4)); 305 } 306 307 static long getHistoryLastUsedKey() { 308 return Config.getPref().getLong(COMMENT_LAST_USED_KEY, 0); 309 } 310 311 /** 312 * Updates the combobox histories when a combobox editor loses focus. 313 * 314 * @param text The {@code JTextField} of the combobox editor. 315 */ 316 private void updateHistory(JTextField text) { 317 String tag = (String) text.getDocument().getProperty("tag"); // tag is either "comment" or "source" 318 if ("comment".equals(tag)) { 319 hcbUploadComment.addCurrentItemToHistory(); 320 } else if ("source".equals(tag)) { 321 hcbUploadSource.addCurrentItemToHistory(); 322 } 323 } 324 325 /** 326 * Updates the table editor model with changes in the comboboxes. 327 * 328 * The lock prevents loops in change notifications, eg. the combobox 329 * notifies the table model and the table model notifies the combobox, which 330 * throws IllegalStateException. 331 * 332 * @param text The {@code JTextField} of the combobox editor. 333 */ 334 private void updateModel(JTextField text) { 335 if (!locked) { 336 locked = true; 337 try { 338 String tag = (String) text.getDocument().getProperty("tag"); // tag is either "comment" or "source" 339 String value = text.getText(); 340 model.put(tag, value.isEmpty() ? null : value); // remove tags with empty values 341 } finally { 342 locked = false; 343 } 344 } 345 } 346 347 /** 348 * Save all outstanding edits to the model. 349 * @see UploadDialog#saveEdits 350 * @since 18173 351 */ 352 public void saveEdits() { 353 updateModel(hcbUploadComment.getEditorComponent()); 354 hcbUploadComment.addCurrentItemToHistory(); 355 updateModel(hcbUploadSource.getEditorComponent()); 356 hcbUploadSource.addCurrentItemToHistory(); 357 } 358 359 /** 360 * Returns the UplodDialog that is our ancestor 361 * 362 * @return the UploadDialog or null 363 */ 364 private UploadDialog getDialog() { 365 Component d = getRootPane(); 366 while ((d = d.getParent()) != null) { 367 if (d instanceof UploadDialog) 368 return (UploadDialog) d; 369 } 370 return null; 371 } 372 373 /** 374 * Update the model when the selection changes in a combobox. 375 * @param e The action event. 376 */ 377 @Override 378 public void actionPerformed(ActionEvent e) { 379 setFocusToUploadButton(); 380 } 381 382 @Override 383 public void focusGained(FocusEvent e) { 384 } 385 386 /** 387 * Update the model and combobox history when a combobox editor loses focus. 388 */ 389 @Override 390 public void focusLost(FocusEvent e) { 391 Object c = e.getSource(); 392 if (c instanceof JTextField) { 393 updateModel((JTextField) c); 394 updateHistory((JTextField) c); 395 } 396 } 397 398 /** 399 * Updates the table editor model upon changes in the "review" checkbox. 400 */ 401 @Override 402 public void itemStateChanged(ItemEvent e) { 403 if (!locked) { 404 locked = true; 405 try { 406 model.put("review_requested", e.getStateChange() == ItemEvent.SELECTED ? "yes" : null); 407 } finally { 408 locked = false; 409 } 410 } 411 } 412 413 /** 414 * Updates the controls upon changes in the table editor model. 415 */ 416 @Override 417 public void tableChanged(TableModelEvent e) { 418 if (!locked) { 419 locked = true; 420 try { 421 hcbUploadComment.setText(get("comment")); 422 hcbUploadSource.setText(get("source")); 423 cbRequestReview.setSelected(get("review_requested").equals("yes")); 424 } finally { 425 locked = false; 426 } 427 } 428 } 429 430 /** 431 * Set the focus directly to the upload button if "Enter" key is pressed in any combobox. 432 */ 433 @Override 434 public void keyTyped(KeyEvent e) { 435 if (e.getKeyChar() == KeyEvent.VK_ENTER) { 436 setFocusToUploadButton(); 437 } 438 } 439 440 @Override 441 public void keyPressed(KeyEvent e) { 442 } 443 444 @Override 445 public void keyReleased(KeyEvent e) { 446 } 447 448 private void setFocusToUploadButton() { 449 Optional.ofNullable(getDialog()).ifPresent(UploadDialog::setFocusToUploadButton); 450 } 451}