001// License: GPL. For details, see LICENSE file. 002package org.openstreetmap.josm.gui.io; 003 004import static org.openstreetmap.josm.gui.help.HelpUtil.ht; 005import static org.openstreetmap.josm.tools.I18n.tr; 006import static org.openstreetmap.josm.tools.I18n.trn; 007 008import java.awt.BorderLayout; 009import java.awt.Component; 010import java.awt.Dimension; 011import java.awt.FlowLayout; 012import java.awt.GridBagLayout; 013import java.awt.Image; 014import java.awt.event.ActionEvent; 015import java.awt.event.KeyEvent; 016import java.awt.event.WindowAdapter; 017import java.awt.event.WindowEvent; 018import java.beans.PropertyChangeEvent; 019import java.beans.PropertyChangeListener; 020import java.util.ArrayList; 021import java.util.Collection; 022import java.util.Collections; 023import java.util.List; 024import java.util.Map; 025import java.util.Map.Entry; 026 027import javax.swing.AbstractAction; 028import javax.swing.BorderFactory; 029import javax.swing.Icon; 030import javax.swing.ImageIcon; 031import javax.swing.JButton; 032import javax.swing.JComponent; 033import javax.swing.JDialog; 034import javax.swing.JOptionPane; 035import javax.swing.JPanel; 036import javax.swing.JTabbedPane; 037import javax.swing.KeyStroke; 038 039import org.openstreetmap.josm.Main; 040import org.openstreetmap.josm.data.APIDataSet; 041import org.openstreetmap.josm.data.Preferences.PreferenceChangeEvent; 042import org.openstreetmap.josm.data.Preferences.PreferenceChangedListener; 043import org.openstreetmap.josm.data.Preferences.Setting; 044import org.openstreetmap.josm.data.osm.Changeset; 045import org.openstreetmap.josm.data.osm.OsmPrimitive; 046import org.openstreetmap.josm.gui.ExtendedDialog; 047import org.openstreetmap.josm.gui.HelpAwareOptionPane; 048import org.openstreetmap.josm.gui.SideButton; 049import org.openstreetmap.josm.gui.help.ContextSensitiveHelpAction; 050import org.openstreetmap.josm.gui.help.HelpUtil; 051import org.openstreetmap.josm.io.OsmApi; 052import org.openstreetmap.josm.tools.GBC; 053import org.openstreetmap.josm.tools.ImageProvider; 054import org.openstreetmap.josm.tools.InputMapUtils; 055import org.openstreetmap.josm.tools.Utils; 056import org.openstreetmap.josm.tools.WindowGeometry; 057 058/** 059 * This is a dialog for entering upload options like the parameters for 060 * the upload changeset and the strategy for opening/closing a changeset. 061 * 062 */ 063public class UploadDialog extends JDialog implements PropertyChangeListener, PreferenceChangedListener{ 064 /** the unique instance of the upload dialog */ 065 static private UploadDialog uploadDialog; 066 067 /** 068 * List of custom components that can be added by plugins at JOSM startup. 069 */ 070 static private final Collection<Component> customComponents = new ArrayList<Component>(); 071 072 /** 073 * Replies the unique instance of the upload dialog 074 * 075 * @return the unique instance of the upload dialog 076 */ 077 static public UploadDialog getUploadDialog() { 078 if (uploadDialog == null) { 079 uploadDialog = new UploadDialog(); 080 } 081 return uploadDialog; 082 } 083 084 /** the panel with the objects to upload */ 085 private UploadedObjectsSummaryPanel pnlUploadedObjects; 086 /** the panel to select the changeset used */ 087 private ChangesetManagementPanel pnlChangesetManagement; 088 089 private BasicUploadSettingsPanel pnlBasicUploadSettings; 090 091 private UploadStrategySelectionPanel pnlUploadStrategySelectionPanel; 092 093 /** checkbox for selecting whether an atomic upload is to be used */ 094 private TagSettingsPanel pnlTagSettings; 095 /** the tabbed pane used below of the list of primitives */ 096 private JTabbedPane tpConfigPanels; 097 /** the upload button */ 098 private JButton btnUpload; 099 private boolean canceled = false; 100 101 /** the changeset comment model keeping the state of the changeset comment */ 102 private final ChangesetCommentModel changesetCommentModel = new ChangesetCommentModel(); 103 private final ChangesetCommentModel changesetSourceModel = new ChangesetCommentModel(); 104 105 /** 106 * builds the content panel for the upload dialog 107 * 108 * @return the content panel 109 */ 110 protected JPanel buildContentPanel() { 111 JPanel pnl = new JPanel(new GridBagLayout()); 112 pnl.setBorder(BorderFactory.createEmptyBorder(5,5,5,5)); 113 114 // the panel with the list of uploaded objects 115 // 116 pnl.add(pnlUploadedObjects = new UploadedObjectsSummaryPanel(), GBC.eol().fill(GBC.BOTH)); 117 118 // Custom components 119 for (Component c : customComponents) { 120 pnl.add(c, GBC.eol().fill(GBC.HORIZONTAL)); 121 } 122 123 // a tabbed pane with configuration panels in the lower half 124 // 125 tpConfigPanels = new JTabbedPane() { 126 @Override 127 public Dimension getPreferredSize() { 128 // make sure the tabbed pane never grabs more space than necessary 129 // 130 return super.getMinimumSize(); 131 } 132 }; 133 134 tpConfigPanels.add(pnlBasicUploadSettings = new BasicUploadSettingsPanel(changesetCommentModel, changesetSourceModel)); 135 tpConfigPanels.setTitleAt(0, tr("Settings")); 136 tpConfigPanels.setToolTipTextAt(0, tr("Decide how to upload the data and which changeset to use")); 137 138 tpConfigPanels.add(pnlTagSettings = new TagSettingsPanel(changesetCommentModel, changesetSourceModel)); 139 tpConfigPanels.setTitleAt(1, tr("Tags of new changeset")); 140 tpConfigPanels.setToolTipTextAt(1, tr("Apply tags to the changeset data is uploaded to")); 141 142 tpConfigPanels.add(pnlChangesetManagement = new ChangesetManagementPanel(changesetCommentModel)); 143 tpConfigPanels.setTitleAt(2, tr("Changesets")); 144 tpConfigPanels.setToolTipTextAt(2, tr("Manage open changesets and select a changeset to upload to")); 145 146 tpConfigPanels.add(pnlUploadStrategySelectionPanel = new UploadStrategySelectionPanel()); 147 tpConfigPanels.setTitleAt(3, tr("Advanced")); 148 tpConfigPanels.setToolTipTextAt(3, tr("Configure advanced settings")); 149 150 pnl.add(tpConfigPanels, GBC.eol().fill(GBC.HORIZONTAL)); 151 return pnl; 152 } 153 154 /** 155 * builds the panel with the OK and CANCEL buttons 156 * 157 * @return The panel with the OK and CANCEL buttons 158 */ 159 protected JPanel buildActionPanel() { 160 JPanel pnl = new JPanel(); 161 pnl.setLayout(new FlowLayout(FlowLayout.CENTER)); 162 pnl.setBorder(BorderFactory.createEmptyBorder(5,5,5,5)); 163 164 // -- upload button 165 UploadAction uploadAction = new UploadAction(); 166 pnl.add(btnUpload = new SideButton(uploadAction)); 167 btnUpload.setFocusable(true); 168 InputMapUtils.enableEnter(btnUpload); 169 170 // -- cancel button 171 CancelAction cancelAction = new CancelAction(); 172 pnl.add(new SideButton(cancelAction)); 173 getRootPane().registerKeyboardAction( 174 cancelAction, 175 KeyStroke.getKeyStroke(KeyEvent.VK_ESCAPE,0), 176 JComponent.WHEN_IN_FOCUSED_WINDOW 177 ); 178 pnl.add(new SideButton(new ContextSensitiveHelpAction(ht("/Dialog/Upload")))); 179 HelpUtil.setHelpContext(getRootPane(),ht("/Dialog/Upload")); 180 return pnl; 181 } 182 183 /** 184 * builds the gui 185 */ 186 protected void build() { 187 setTitle(tr("Upload to ''{0}''", OsmApi.getOsmApi().getBaseUrl())); 188 getContentPane().setLayout(new BorderLayout()); 189 getContentPane().add(buildContentPanel(), BorderLayout.CENTER); 190 getContentPane().add(buildActionPanel(), BorderLayout.SOUTH); 191 192 addWindowListener(new WindowEventHandler()); 193 194 195 // make sure the configuration panels listen to each other 196 // changes 197 // 198 pnlChangesetManagement.addPropertyChangeListener( 199 pnlBasicUploadSettings.getUploadParameterSummaryPanel() 200 ); 201 pnlChangesetManagement.addPropertyChangeListener(this); 202 pnlUploadedObjects.addPropertyChangeListener( 203 pnlBasicUploadSettings.getUploadParameterSummaryPanel() 204 ); 205 pnlUploadedObjects.addPropertyChangeListener(pnlUploadStrategySelectionPanel); 206 pnlUploadStrategySelectionPanel.addPropertyChangeListener( 207 pnlBasicUploadSettings.getUploadParameterSummaryPanel() 208 ); 209 210 211 // users can click on either of two links in the upload parameter 212 // summary handler. This installs the handler for these two events. 213 // We simply select the appropriate tab in the tabbed pane with the 214 // configuration dialogs. 215 // 216 pnlBasicUploadSettings.getUploadParameterSummaryPanel().setConfigurationParameterRequestListener( 217 new ConfigurationParameterRequestHandler() { 218 @Override 219 public void handleUploadStrategyConfigurationRequest() { 220 tpConfigPanels.setSelectedIndex(3); 221 } 222 @Override 223 public void handleChangesetConfigurationRequest() { 224 tpConfigPanels.setSelectedIndex(2); 225 } 226 } 227 ); 228 229 pnlBasicUploadSettings.setUploadTagDownFocusTraversalHandlers( 230 new AbstractAction() { 231 @Override 232 public void actionPerformed(ActionEvent e) { 233 btnUpload.requestFocusInWindow(); 234 } 235 } 236 ); 237 238 Main.pref.addPreferenceChangeListener(this); 239 } 240 241 /** 242 * constructor 243 */ 244 public UploadDialog() { 245 super(JOptionPane.getFrameForComponent(Main.parent), ModalityType.DOCUMENT_MODAL); 246 build(); 247 } 248 249 /** 250 * Sets the collection of primitives to upload 251 * 252 * @param toUpload the dataset with the objects to upload. If null, assumes the empty 253 * set of objects to upload 254 * 255 */ 256 public void setUploadedPrimitives(APIDataSet toUpload) { 257 if (toUpload == null) { 258 List<OsmPrimitive> emptyList = Collections.emptyList(); 259 pnlUploadedObjects.setUploadedPrimitives(emptyList, emptyList, emptyList); 260 return; 261 } 262 pnlUploadedObjects.setUploadedPrimitives( 263 toUpload.getPrimitivesToAdd(), 264 toUpload.getPrimitivesToUpdate(), 265 toUpload.getPrimitivesToDelete() 266 ); 267 } 268 269 /** 270 * Remembers the user input in the preference settings 271 */ 272 public void rememberUserInput() { 273 pnlBasicUploadSettings.rememberUserInput(); 274 pnlUploadStrategySelectionPanel.rememberUserInput(); 275 } 276 277 /** 278 * Initializes the panel for user input 279 */ 280 public void startUserInput() { 281 tpConfigPanels.setSelectedIndex(0); 282 pnlBasicUploadSettings.startUserInput(); 283 pnlTagSettings.startUserInput(); 284 pnlTagSettings.initFromChangeset(pnlChangesetManagement.getSelectedChangeset()); 285 pnlUploadStrategySelectionPanel.initFromPreferences(); 286 UploadParameterSummaryPanel pnl = pnlBasicUploadSettings.getUploadParameterSummaryPanel(); 287 pnl.setUploadStrategySpecification(pnlUploadStrategySelectionPanel.getUploadStrategySpecification()); 288 pnl.setCloseChangesetAfterNextUpload(pnlChangesetManagement.isCloseChangesetAfterUpload()); 289 pnl.setNumObjects(pnlUploadedObjects.getNumObjectsToUpload()); 290 } 291 292 /** 293 * Replies the current changeset 294 * 295 * @return the current changeset 296 */ 297 public Changeset getChangeset() { 298 Changeset cs = pnlChangesetManagement.getSelectedChangeset(); 299 if (cs == null) { 300 cs = new Changeset(); 301 } 302 cs.setKeys(pnlTagSettings.getTags(false)); 303 return cs; 304 } 305 306 public void setSelectedChangesetForNextUpload(Changeset cs) { 307 pnlChangesetManagement.setSelectedChangesetForNextUpload(cs); 308 } 309 310 public Map<String, String> getDefaultChangesetTags() { 311 return pnlTagSettings.getDefaultTags(); 312 } 313 314 public void setDefaultChangesetTags(Map<String, String> tags) { 315 pnlTagSettings.setDefaultTags(tags); 316 for (Entry<String, String> entry: tags.entrySet()) { 317 if ("comment".equals(entry.getKey())) { 318 changesetCommentModel.setComment(entry.getValue()); 319 } 320 } 321 } 322 323 /** 324 * Replies the {@link UploadStrategySpecification} the user entered in the dialog. 325 * 326 * @return the {@link UploadStrategySpecification} the user entered in the dialog. 327 */ 328 public UploadStrategySpecification getUploadStrategySpecification() { 329 UploadStrategySpecification spec = pnlUploadStrategySelectionPanel.getUploadStrategySpecification(); 330 spec.setCloseChangesetAfterUpload(pnlChangesetManagement.isCloseChangesetAfterUpload()); 331 return spec; 332 } 333 334 /** 335 * Returns the current value for the upload comment 336 * 337 * @return the current value for the upload comment 338 */ 339 protected String getUploadComment() { 340 return changesetCommentModel.getComment(); 341 } 342 343 /** 344 * Returns the current value for the changeset source 345 * 346 * @return the current value for the changeset source 347 */ 348 protected String getUploadSource() { 349 return changesetSourceModel.getComment(); 350 } 351 352 /** 353 * Returns true if the dialog was canceled 354 * 355 * @return true if the dialog was canceled 356 */ 357 public boolean isCanceled() { 358 return canceled; 359 } 360 361 /** 362 * Sets whether the dialog was canceled 363 * 364 * @param canceled true if the dialog is canceled 365 */ 366 protected void setCanceled(boolean canceled) { 367 this.canceled = canceled; 368 } 369 370 @Override 371 public void setVisible(boolean visible) { 372 if (visible) { 373 new WindowGeometry( 374 getClass().getName() + ".geometry", 375 WindowGeometry.centerInWindow( 376 Main.parent, 377 new Dimension(400,600) 378 ) 379 ).applySafe(this); 380 startUserInput(); 381 } else if (isShowing()) { // Avoid IllegalComponentStateException like in #8775 382 new WindowGeometry(this).remember(getClass().getName() + ".geometry"); 383 } 384 super.setVisible(visible); 385 } 386 387 /** 388 * Adds a custom component to this dialog. 389 * Custom components added at JOSM startup are displayed between the objects list and the properties tab pane. 390 * @param c The custom component to add. If {@code null}, this method does nothing. 391 * @return {@code true} if the collection of custom components changed as a result of the call 392 * @since 5842 393 */ 394 public static boolean addCustomComponent(Component c) { 395 if (c != null) { 396 return customComponents.add(c); 397 } 398 return false; 399 } 400 401 /** 402 * Handles an upload 403 * 404 */ 405 class UploadAction extends AbstractAction { 406 public UploadAction() { 407 putValue(NAME, tr("Upload Changes")); 408 putValue(SMALL_ICON, ImageProvider.get("upload")); 409 putValue(SHORT_DESCRIPTION, tr("Upload the changed primitives")); 410 } 411 412 /** 413 * Displays a warning message indicating that the upload comment is empty/short. 414 * @return true if the user wants to revisit, false if they want to continue 415 */ 416 protected boolean warnUploadComment() { 417 return warnUploadTag( 418 tr("Please revise upload comment"), 419 tr("Your upload comment is <i>empty</i>, or <i>very short</i>.<br /><br />" + 420 "This is technically allowed, but please consider that many users who are<br />" + 421 "watching changes in their area depend on meaningful changeset comments<br />" + 422 "to understand what is going on!<br /><br />" + 423 "If you spend a minute now to explain your change, you will make life<br />" + 424 "easier for many other mappers."), 425 "upload_comment_is_empty_or_very_short" 426 ); 427 } 428 429 /** 430 * Displays a warning message indicating that no changeset source is given. 431 * @return true if the user wants to revisit, false if they want to continue 432 */ 433 protected boolean warnUploadSource() { 434 return warnUploadTag( 435 tr("Please specify a changeset source"), 436 tr("You did not specify a source for your changes.<br />" + 437 "This is technically allowed, but it assists other users <br />" + 438 "to understand the origins of the data.<br /><br />" + 439 "If you spend a minute now to explain your change, you will make life<br />" + 440 "easier for many other mappers."), 441 "upload_source_is_empty" 442 ); 443 } 444 445 protected boolean warnUploadTag(final String title, final String message, final String togglePref) { 446 ExtendedDialog dlg = new ExtendedDialog(UploadDialog.this, 447 title, 448 new String[] {tr("Revise"), tr("Cancel"), tr("Continue as is")}); 449 dlg.setContent("<html>" + message + "</html>"); 450 dlg.setButtonIcons(new Icon[] { 451 ImageProvider.get("ok"), 452 ImageProvider.get("cancel"), 453 ImageProvider.overlay( 454 ImageProvider.get("upload"), 455 new ImageIcon(ImageProvider.get("warning-small").getImage().getScaledInstance(10 , 10, Image.SCALE_SMOOTH)), 456 ImageProvider.OverlayPosition.SOUTHEAST)}); 457 dlg.setToolTipTexts(new String[] { 458 tr("Return to the previous dialog to enter a more descriptive comment"), 459 tr("Cancel and return to the previous dialog"), 460 tr("Ignore this hint and upload anyway")}); 461 dlg.setIcon(JOptionPane.WARNING_MESSAGE); 462 dlg.toggleEnable(togglePref); 463 dlg.setToggleCheckboxText(tr("Do not show this message again")); 464 dlg.setCancelButton(1, 2); 465 return dlg.showDialog().getValue() != 3; 466 } 467 468 protected void warnIllegalChunkSize() { 469 HelpAwareOptionPane.showOptionDialog( 470 UploadDialog.this, 471 tr("Please enter a valid chunk size first"), 472 tr("Illegal chunk size"), 473 JOptionPane.ERROR_MESSAGE, 474 ht("/Dialog/Upload#IllegalChunkSize") 475 ); 476 } 477 478 @Override 479 public void actionPerformed(ActionEvent e) { 480 if ((getUploadComment().trim().length() < 10 && warnUploadComment()) /* abort for missing comment */ 481 || (getUploadSource().trim().isEmpty() && warnUploadSource()) /* abort for missing changeset source */ 482 ) { 483 tpConfigPanels.setSelectedIndex(0); 484 pnlBasicUploadSettings.initEditingOfUploadComment(); 485 return; 486 } 487 488 /* test for empty tags in the changeset metadata and proceed only after user's confirmation. 489 * though, accept if key and value are empty (cf. xor). */ 490 List<String> emptyChangesetTags = new ArrayList<String>(); 491 for (final Entry<String, String> i : pnlTagSettings.getTags(true).entrySet()) { 492 final boolean isKeyEmpty = i.getKey() == null || i.getKey().trim().isEmpty(); 493 final boolean isValueEmpty = i.getValue() == null || i.getValue().trim().isEmpty(); 494 final boolean ignoreKey = "comment".equals(i.getKey()) || "source".equals(i.getKey()); 495 if ((isKeyEmpty ^ isValueEmpty) && !ignoreKey) { 496 emptyChangesetTags.add(tr("{0}={1}", i.getKey(), i.getValue())); 497 } 498 } 499 if (!emptyChangesetTags.isEmpty() && JOptionPane.OK_OPTION != JOptionPane.showConfirmDialog( 500 Main.parent, 501 trn( 502 "<html>The following changeset tag contains an empty key/value:<br>{0}<br>Continue?</html>", 503 "<html>The following changeset tags contain an empty key/value:<br>{0}<br>Continue?</html>", 504 emptyChangesetTags.size(), Utils.joinAsHtmlUnorderedList(emptyChangesetTags)), 505 tr("Empty metadata"), 506 JOptionPane.OK_CANCEL_OPTION, 507 JOptionPane.WARNING_MESSAGE 508 )) { 509 tpConfigPanels.setSelectedIndex(0); 510 pnlBasicUploadSettings.initEditingOfUploadComment(); 511 return; 512 } 513 514 UploadStrategySpecification strategy = getUploadStrategySpecification(); 515 if (strategy.getStrategy().equals(UploadStrategy.CHUNKED_DATASET_STRATEGY)) { 516 if (strategy.getChunkSize() == UploadStrategySpecification.UNSPECIFIED_CHUNK_SIZE) { 517 warnIllegalChunkSize(); 518 tpConfigPanels.setSelectedIndex(0); 519 return; 520 } 521 } 522 setCanceled(false); 523 setVisible(false); 524 } 525 } 526 527 /** 528 * Action for canceling the dialog 529 * 530 */ 531 class CancelAction extends AbstractAction { 532 public CancelAction() { 533 putValue(NAME, tr("Cancel")); 534 putValue(SMALL_ICON, ImageProvider.get("cancel")); 535 putValue(SHORT_DESCRIPTION, tr("Cancel the upload and resume editing")); 536 } 537 538 @Override 539 public void actionPerformed(ActionEvent e) { 540 setCanceled(true); 541 setVisible(false); 542 } 543 } 544 545 /** 546 * Listens to window closing events and processes them as cancel events. 547 * Listens to window open events and initializes user input 548 * 549 */ 550 class WindowEventHandler extends WindowAdapter { 551 @Override 552 public void windowClosing(WindowEvent e) { 553 setCanceled(true); 554 } 555 556 @Override 557 public void windowActivated(WindowEvent arg0) { 558 if (tpConfigPanels.getSelectedIndex() == 0) { 559 pnlBasicUploadSettings.initEditingOfUploadComment(); 560 } 561 } 562 } 563 564 /* -------------------------------------------------------------------------- */ 565 /* Interface PropertyChangeListener */ 566 /* -------------------------------------------------------------------------- */ 567 @Override 568 public void propertyChange(PropertyChangeEvent evt) { 569 if (evt.getPropertyName().equals(ChangesetManagementPanel.SELECTED_CHANGESET_PROP)) { 570 Changeset cs = (Changeset)evt.getNewValue(); 571 if (cs == null) { 572 tpConfigPanels.setTitleAt(1, tr("Tags of new changeset")); 573 } else { 574 tpConfigPanels.setTitleAt(1, tr("Tags of changeset {0}", cs.getId())); 575 } 576 } 577 } 578 579 /* -------------------------------------------------------------------------- */ 580 /* Interface PreferenceChangedListener */ 581 /* -------------------------------------------------------------------------- */ 582 @Override 583 public void preferenceChanged(PreferenceChangeEvent e) { 584 if (e.getKey() == null || ! e.getKey().equals("osm-server.url")) 585 return; 586 final Setting<?> newValue = e.getNewValue(); 587 final String url; 588 if (newValue == null || newValue.getValue() == null) { 589 url = OsmApi.getOsmApi().getBaseUrl(); 590 } else { 591 url = newValue.getValue().toString(); 592 } 593 setTitle(tr("Upload to ''{0}''", url)); 594 } 595}