001// License: GPL. For details, see LICENSE file. 002package org.openstreetmap.josm.gui.io; 003 004import static org.openstreetmap.josm.tools.I18n.tr; 005import static org.openstreetmap.josm.tools.I18n.trn; 006 007import java.awt.BorderLayout; 008import java.awt.Component; 009import java.awt.Dimension; 010import java.awt.FlowLayout; 011import java.awt.Graphics2D; 012import java.awt.GridBagConstraints; 013import java.awt.GridBagLayout; 014import java.awt.Image; 015import java.awt.event.ActionEvent; 016import java.awt.event.WindowAdapter; 017import java.awt.event.WindowEvent; 018import java.awt.image.BufferedImage; 019import java.beans.PropertyChangeEvent; 020import java.beans.PropertyChangeListener; 021import java.util.List; 022import java.util.concurrent.CancellationException; 023import java.util.concurrent.ExecutorService; 024import java.util.concurrent.Executors; 025import java.util.concurrent.Future; 026 027import javax.swing.AbstractAction; 028import javax.swing.DefaultListCellRenderer; 029import javax.swing.ImageIcon; 030import javax.swing.JButton; 031import javax.swing.JComponent; 032import javax.swing.JDialog; 033import javax.swing.JLabel; 034import javax.swing.JList; 035import javax.swing.JOptionPane; 036import javax.swing.JPanel; 037import javax.swing.JScrollPane; 038import javax.swing.KeyStroke; 039import javax.swing.ListCellRenderer; 040import javax.swing.WindowConstants; 041import javax.swing.event.TableModelEvent; 042import javax.swing.event.TableModelListener; 043 044import org.openstreetmap.josm.Main; 045import org.openstreetmap.josm.actions.UploadAction; 046import org.openstreetmap.josm.data.APIDataSet; 047import org.openstreetmap.josm.gui.ExceptionDialogUtil; 048import org.openstreetmap.josm.gui.io.SaveLayersModel.Mode; 049import org.openstreetmap.josm.gui.progress.ProgressMonitor; 050import org.openstreetmap.josm.gui.progress.SwingRenderingProgressMonitor; 051import org.openstreetmap.josm.gui.util.GuiHelper; 052import org.openstreetmap.josm.tools.ImageProvider; 053import org.openstreetmap.josm.tools.WindowGeometry; 054 055public class SaveLayersDialog extends JDialog implements TableModelListener { 056 public static enum UserAction { 057 /** save/upload layers was successful, proceed with operation */ 058 PROCEED, 059 /** save/upload of layers was not successful or user canceled operation */ 060 CANCEL 061 } 062 063 private SaveLayersModel model; 064 private UserAction action = UserAction.CANCEL; 065 private UploadAndSaveProgressRenderer pnlUploadLayers; 066 067 private SaveAndProceedAction saveAndProceedAction; 068 private DiscardAndProceedAction discardAndProceedAction; 069 private CancelAction cancelAction; 070 private SaveAndUploadTask saveAndUploadTask; 071 072 /** 073 * builds the GUI 074 */ 075 protected void build() { 076 WindowGeometry geometry = WindowGeometry.centerOnScreen(new Dimension(650,300)); 077 geometry.applySafe(this); 078 getContentPane().setLayout(new BorderLayout()); 079 080 model = new SaveLayersModel(); 081 SaveLayersTable table = new SaveLayersTable(model); 082 JScrollPane pane = new JScrollPane(table); 083 model.addPropertyChangeListener(table); 084 table.getModel().addTableModelListener(this); 085 086 getContentPane().add(pane, BorderLayout.CENTER); 087 getContentPane().add(buildButtonRow(), BorderLayout.SOUTH); 088 089 addWindowListener(new WindowClosingAdapter()); 090 setDefaultCloseOperation(WindowConstants.DISPOSE_ON_CLOSE); 091 } 092 093 private JButton saveAndProceedActionButton = null; 094 095 /** 096 * builds the button row 097 * 098 * @return the panel with the button row 099 */ 100 protected JPanel buildButtonRow() { 101 JPanel pnl = new JPanel(); 102 pnl.setLayout(new FlowLayout(FlowLayout.CENTER)); 103 104 saveAndProceedAction = new SaveAndProceedAction(); 105 model.addPropertyChangeListener(saveAndProceedAction); 106 pnl.add(saveAndProceedActionButton = new JButton(saveAndProceedAction)); 107 108 discardAndProceedAction = new DiscardAndProceedAction(); 109 model.addPropertyChangeListener(discardAndProceedAction); 110 pnl.add(new JButton(discardAndProceedAction)); 111 112 cancelAction = new CancelAction(); 113 pnl.add(new JButton(cancelAction)); 114 115 JPanel pnl2 = new JPanel(); 116 pnl2.setLayout(new BorderLayout()); 117 pnl2.add(pnlUploadLayers = new UploadAndSaveProgressRenderer(), BorderLayout.CENTER); 118 model.addPropertyChangeListener(pnlUploadLayers); 119 pnl2.add(pnl, BorderLayout.SOUTH); 120 return pnl2; 121 } 122 123 public void prepareForSavingAndUpdatingLayersBeforeExit() { 124 setTitle(tr("Unsaved changes - Save/Upload before exiting?")); 125 this.saveAndProceedAction.initForSaveAndExit(); 126 this.discardAndProceedAction.initForDiscardAndExit(); 127 } 128 129 public void prepareForSavingAndUpdatingLayersBeforeDelete() { 130 setTitle(tr("Unsaved changes - Save/Upload before deleting?")); 131 this.saveAndProceedAction.initForSaveAndDelete(); 132 this.discardAndProceedAction.initForDiscardAndDelete(); 133 } 134 135 public SaveLayersDialog(Component parent) { 136 super(JOptionPane.getFrameForComponent(parent), ModalityType.DOCUMENT_MODAL); 137 build(); 138 } 139 140 public UserAction getUserAction() { 141 return this.action; 142 } 143 144 public SaveLayersModel getModel() { 145 return model; 146 } 147 148 protected void launchSafeAndUploadTask() { 149 ProgressMonitor monitor = new SwingRenderingProgressMonitor(pnlUploadLayers); 150 monitor.beginTask(tr("Uploading and saving modified layers ...")); 151 this.saveAndUploadTask = new SaveAndUploadTask(model, monitor); 152 new Thread(saveAndUploadTask).start(); 153 } 154 155 protected void cancelSafeAndUploadTask() { 156 if (this.saveAndUploadTask != null) { 157 this.saveAndUploadTask.cancel(); 158 } 159 model.setMode(Mode.EDITING_DATA); 160 } 161 162 private static class LayerListWarningMessagePanel extends JPanel { 163 private JLabel lblMessage; 164 private JList<SaveLayerInfo> lstLayers; 165 166 protected void build() { 167 setLayout(new GridBagLayout()); 168 GridBagConstraints gc = new GridBagConstraints(); 169 gc.gridx = 0; 170 gc.gridy = 0; 171 gc.fill = GridBagConstraints.HORIZONTAL; 172 gc.weightx = 1.0; 173 gc.weighty = 0.0; 174 add(lblMessage = new JLabel(), gc); 175 lblMessage.setHorizontalAlignment(JLabel.LEFT); 176 lstLayers = new JList<>(); 177 lstLayers.setCellRenderer( 178 new ListCellRenderer<SaveLayerInfo>() { 179 final DefaultListCellRenderer def = new DefaultListCellRenderer(); 180 @Override 181 public Component getListCellRendererComponent(JList<? extends SaveLayerInfo> list, SaveLayerInfo info, int index, 182 boolean isSelected, boolean cellHasFocus) { 183 def.setIcon(info.getLayer().getIcon()); 184 def.setText(info.getName()); 185 return def; 186 } 187 } 188 ); 189 gc.gridx = 0; 190 gc.gridy = 1; 191 gc.fill = GridBagConstraints.HORIZONTAL; 192 gc.weightx = 1.0; 193 gc.weighty = 1.0; 194 add(lstLayers,gc); 195 } 196 197 public LayerListWarningMessagePanel(String msg, List<SaveLayerInfo> infos) { 198 build(); 199 lblMessage.setText(msg); 200 lstLayers.setListData(infos.toArray(new SaveLayerInfo[0])); 201 } 202 } 203 204 protected void warnLayersWithConflictsAndUploadRequest(List<SaveLayerInfo> infos) { 205 String msg = trn("<html>{0} layer has unresolved conflicts.<br>" 206 + "Either resolve them first or discard the modifications.<br>" 207 + "Layer with conflicts:</html>", 208 "<html>{0} layers have unresolved conflicts.<br>" 209 + "Either resolve them first or discard the modifications.<br>" 210 + "Layers with conflicts:</html>", 211 infos.size(), 212 infos.size()); 213 JOptionPane.showConfirmDialog( 214 Main.parent, 215 new LayerListWarningMessagePanel(msg, infos), 216 tr("Unsaved data and conflicts"), 217 JOptionPane.DEFAULT_OPTION, 218 JOptionPane.WARNING_MESSAGE 219 ); 220 } 221 222 protected void warnLayersWithoutFilesAndSaveRequest(List<SaveLayerInfo> infos) { 223 String msg = trn("<html>{0} layer needs saving but has no associated file.<br>" 224 + "Either select a file for this layer or discard the changes.<br>" 225 + "Layer without a file:</html>", 226 "<html>{0} layers need saving but have no associated file.<br>" 227 + "Either select a file for each of them or discard the changes.<br>" 228 + "Layers without a file:</html>", 229 infos.size(), 230 infos.size()); 231 JOptionPane.showConfirmDialog( 232 Main.parent, 233 new LayerListWarningMessagePanel(msg, infos), 234 tr("Unsaved data and missing associated file"), 235 JOptionPane.DEFAULT_OPTION, 236 JOptionPane.WARNING_MESSAGE 237 ); 238 } 239 240 protected void warnLayersWithIllegalFilesAndSaveRequest(List<SaveLayerInfo> infos) { 241 String msg = trn("<html>{0} layer needs saving but has an associated file<br>" 242 + "which cannot be written.<br>" 243 + "Either select another file for this layer or discard the changes.<br>" 244 + "Layer with a non-writable file:</html>", 245 "<html>{0} layers need saving but have associated files<br>" 246 + "which cannot be written.<br>" 247 + "Either select another file for each of them or discard the changes.<br>" 248 + "Layers with non-writable files:</html>", 249 infos.size(), 250 infos.size()); 251 JOptionPane.showConfirmDialog( 252 Main.parent, 253 new LayerListWarningMessagePanel(msg, infos), 254 tr("Unsaved data non-writable files"), 255 JOptionPane.DEFAULT_OPTION, 256 JOptionPane.WARNING_MESSAGE 257 ); 258 } 259 260 protected boolean confirmSaveLayerInfosOK() { 261 List<SaveLayerInfo> layerInfos = model.getLayersWithConflictsAndUploadRequest(); 262 if (!layerInfos.isEmpty()) { 263 warnLayersWithConflictsAndUploadRequest(layerInfos); 264 return false; 265 } 266 267 layerInfos = model.getLayersWithoutFilesAndSaveRequest(); 268 if (!layerInfos.isEmpty()) { 269 warnLayersWithoutFilesAndSaveRequest(layerInfos); 270 return false; 271 } 272 273 layerInfos = model.getLayersWithIllegalFilesAndSaveRequest(); 274 if (!layerInfos.isEmpty()) { 275 warnLayersWithIllegalFilesAndSaveRequest(layerInfos); 276 return false; 277 } 278 279 return true; 280 } 281 282 protected void setUserAction(UserAction action) { 283 this.action = action; 284 } 285 286 /** 287 * Closes this dialog and frees all native screen resources. 288 */ 289 public void closeDialog() { 290 setVisible(false); 291 dispose(); 292 } 293 294 class WindowClosingAdapter extends WindowAdapter { 295 @Override 296 public void windowClosing(WindowEvent e) { 297 cancelAction.cancel(); 298 } 299 } 300 301 class CancelAction extends AbstractAction { 302 public CancelAction() { 303 putValue(NAME, tr("Cancel")); 304 putValue(SHORT_DESCRIPTION, tr("Close this dialog and resume editing in JOSM")); 305 putValue(SMALL_ICON, ImageProvider.get("cancel")); 306 getRootPane().getInputMap(JComponent.WHEN_IN_FOCUSED_WINDOW) 307 .put(KeyStroke.getKeyStroke("ESCAPE"), "ESCAPE"); 308 getRootPane().getActionMap().put("ESCAPE", this); 309 } 310 311 protected void cancelWhenInEditingModel() { 312 setUserAction(UserAction.CANCEL); 313 closeDialog(); 314 } 315 316 protected void cancelWhenInSaveAndUploadingMode() { 317 cancelSafeAndUploadTask(); 318 } 319 320 public void cancel() { 321 switch(model.getMode()) { 322 case EDITING_DATA: cancelWhenInEditingModel(); break; 323 case UPLOADING_AND_SAVING: cancelSafeAndUploadTask(); break; 324 } 325 } 326 327 @Override 328 public void actionPerformed(ActionEvent e) { 329 cancel(); 330 } 331 } 332 333 class DiscardAndProceedAction extends AbstractAction implements PropertyChangeListener { 334 public DiscardAndProceedAction() { 335 initForDiscardAndExit(); 336 } 337 338 public void initForDiscardAndExit() { 339 putValue(NAME, tr("Exit now!")); 340 putValue(SHORT_DESCRIPTION, tr("Exit JOSM without saving. Unsaved changes are lost.")); 341 putValue(SMALL_ICON, ImageProvider.get("exit")); 342 } 343 344 public void initForDiscardAndDelete() { 345 putValue(NAME, tr("Delete now!")); 346 putValue(SHORT_DESCRIPTION, tr("Delete layers without saving. Unsaved changes are lost.")); 347 putValue(SMALL_ICON, ImageProvider.get("dialogs", "delete")); 348 } 349 350 @Override 351 public void actionPerformed(ActionEvent e) { 352 setUserAction(UserAction.PROCEED); 353 closeDialog(); 354 } 355 @Override 356 public void propertyChange(PropertyChangeEvent evt) { 357 if (evt.getPropertyName().equals(SaveLayersModel.MODE_PROP)) { 358 Mode mode = (Mode)evt.getNewValue(); 359 switch(mode) { 360 case EDITING_DATA: setEnabled(true); break; 361 case UPLOADING_AND_SAVING: setEnabled(false); break; 362 } 363 } 364 } 365 } 366 367 final class SaveAndProceedAction extends AbstractAction implements PropertyChangeListener { 368 private static final int is = 24; // icon size 369 private static final String BASE_ICON = "BASE_ICON"; 370 private final Image save = ImageProvider.get("save").getImage(); 371 private final Image upld = ImageProvider.get("upload").getImage(); 372 private final Image saveDis = new BufferedImage(is, is, BufferedImage.TYPE_4BYTE_ABGR); 373 private final Image upldDis = new BufferedImage(is, is, BufferedImage.TYPE_4BYTE_ABGR); 374 375 public SaveAndProceedAction() { 376 // get disabled versions of icons 377 new JLabel(ImageProvider.get("save")).getDisabledIcon().paintIcon(new JPanel(), saveDis.getGraphics(), 0, 0); 378 new JLabel(ImageProvider.get("upload")).getDisabledIcon().paintIcon(new JPanel(), upldDis.getGraphics(), 0, 0); 379 initForSaveAndExit(); 380 } 381 382 public void initForSaveAndExit() { 383 putValue(NAME, tr("Perform actions before exiting")); 384 putValue(SHORT_DESCRIPTION, tr("Exit JOSM with saving. Unsaved changes are uploaded and/or saved.")); 385 putValue(BASE_ICON, ImageProvider.get("exit")); 386 redrawIcon(); 387 } 388 389 public void initForSaveAndDelete() { 390 putValue(NAME, tr("Perform actions before deleting")); 391 putValue(SHORT_DESCRIPTION, tr("Save/Upload layers before deleting. Unsaved changes are not lost.")); 392 putValue(BASE_ICON, ImageProvider.get("dialogs", "delete")); 393 redrawIcon(); 394 } 395 396 public void redrawIcon() { 397 try { // Can fail if model is not yet setup properly 398 Image base = ((ImageIcon) getValue(BASE_ICON)).getImage(); 399 BufferedImage newIco = new BufferedImage(is*3, is, BufferedImage.TYPE_4BYTE_ABGR); 400 Graphics2D g = newIco.createGraphics(); 401 g.drawImage(model.getLayersToUpload().isEmpty() ? upldDis : upld, is*0, 0, is, is, null); 402 g.drawImage(model.getLayersToSave().isEmpty() ? saveDis : save, is*1, 0, is, is, null); 403 g.drawImage(base, is*2, 0, is, is, null); 404 putValue(SMALL_ICON, new ImageIcon(newIco)); 405 } catch(Exception e) { 406 putValue(SMALL_ICON, getValue(BASE_ICON)); 407 } 408 } 409 410 @Override 411 public void actionPerformed(ActionEvent e) { 412 if (! confirmSaveLayerInfosOK()) 413 return; 414 launchSafeAndUploadTask(); 415 } 416 417 @Override 418 public void propertyChange(PropertyChangeEvent evt) { 419 if (evt.getPropertyName().equals(SaveLayersModel.MODE_PROP)) { 420 SaveLayersModel.Mode mode = (SaveLayersModel.Mode)evt.getNewValue(); 421 switch(mode) { 422 case EDITING_DATA: setEnabled(true); break; 423 case UPLOADING_AND_SAVING: setEnabled(false); break; 424 } 425 } 426 } 427 } 428 429 /** 430 * This is the asynchronous task which uploads modified layers to the server and 431 * saves them to files, if requested by the user. 432 * 433 */ 434 protected class SaveAndUploadTask implements Runnable { 435 436 private SaveLayersModel model; 437 private ProgressMonitor monitor; 438 private ExecutorService worker; 439 private boolean canceled; 440 private Future<?> currentFuture; 441 private AbstractIOTask currentTask; 442 443 public SaveAndUploadTask(SaveLayersModel model, ProgressMonitor monitor) { 444 this.model = model; 445 this.monitor = monitor; 446 this.worker = Executors.newSingleThreadExecutor(); 447 } 448 449 protected void uploadLayers(List<SaveLayerInfo> toUpload) { 450 for (final SaveLayerInfo layerInfo: toUpload) { 451 if (canceled) { 452 model.setUploadState(layerInfo.getLayer(), UploadOrSaveState.CANCELED); 453 continue; 454 } 455 monitor.subTask(tr("Preparing layer ''{0}'' for upload ...", layerInfo.getName())); 456 457 if (!new UploadAction().checkPreUploadConditions(layerInfo.getLayer())) { 458 model.setUploadState(layerInfo.getLayer(), UploadOrSaveState.FAILED); 459 continue; 460 } 461 final UploadDialog dialog = UploadDialog.getUploadDialog(); 462 dialog.setUploadedPrimitives(new APIDataSet(layerInfo.getLayer().data)); 463 dialog.setVisible(true); 464 if (dialog.isCanceled()) { 465 model.setUploadState(layerInfo.getLayer(), UploadOrSaveState.CANCELED); 466 continue; 467 } 468 dialog.rememberUserInput(); 469 470 currentTask = new UploadLayerTask( 471 UploadDialog.getUploadDialog().getUploadStrategySpecification(), 472 layerInfo.getLayer(), 473 monitor, 474 UploadDialog.getUploadDialog().getChangeset() 475 ); 476 currentFuture = worker.submit(currentTask); 477 try { 478 // wait for the asynchronous task to complete 479 // 480 currentFuture.get(); 481 } catch(CancellationException e) { 482 model.setUploadState(layerInfo.getLayer(), UploadOrSaveState.CANCELED); 483 } catch(Exception e) { 484 Main.error(e); 485 model.setUploadState(layerInfo.getLayer(), UploadOrSaveState.FAILED); 486 ExceptionDialogUtil.explainException(e); 487 } 488 if (currentTask.isCanceled()) { 489 model.setUploadState(layerInfo.getLayer(), UploadOrSaveState.CANCELED); 490 } else if (currentTask.isFailed()) { 491 Main.error(currentTask.getLastException()); 492 ExceptionDialogUtil.explainException(currentTask.getLastException()); 493 model.setUploadState(layerInfo.getLayer(), UploadOrSaveState.FAILED); 494 } else { 495 model.setUploadState(layerInfo.getLayer(), UploadOrSaveState.OK); 496 } 497 currentTask = null; 498 currentFuture = null; 499 } 500 } 501 502 protected void saveLayers(List<SaveLayerInfo> toSave) { 503 for (final SaveLayerInfo layerInfo: toSave) { 504 if (canceled) { 505 model.setSaveState(layerInfo.getLayer(), UploadOrSaveState.CANCELED); 506 continue; 507 } 508 // Check save preconditions earlier to avoid a blocking reentring call to EDT (see #10086) 509 if (layerInfo.isDoCheckSaveConditions()) { 510 if (!layerInfo.getLayer().checkSaveConditions()) { 511 continue; 512 } 513 layerInfo.setDoCheckSaveConditions(false); 514 } 515 currentTask = new SaveLayerTask(layerInfo, monitor); 516 currentFuture = worker.submit(currentTask); 517 518 try { 519 // wait for the asynchronous task to complete 520 // 521 currentFuture.get(); 522 } catch(CancellationException e) { 523 model.setSaveState(layerInfo.getLayer(), UploadOrSaveState.CANCELED); 524 } catch(Exception e) { 525 Main.error(e); 526 model.setSaveState(layerInfo.getLayer(), UploadOrSaveState.FAILED); 527 ExceptionDialogUtil.explainException(e); 528 } 529 if (currentTask.isCanceled()) { 530 model.setSaveState(layerInfo.getLayer(), UploadOrSaveState.CANCELED); 531 } else if (currentTask.isFailed()) { 532 if (currentTask.getLastException() != null) { 533 Main.error(currentTask.getLastException()); 534 ExceptionDialogUtil.explainException(currentTask.getLastException()); 535 } 536 model.setSaveState(layerInfo.getLayer(), UploadOrSaveState.FAILED); 537 } else { 538 model.setSaveState(layerInfo.getLayer(), UploadOrSaveState.OK); 539 } 540 this.currentTask = null; 541 this.currentFuture = null; 542 } 543 } 544 545 protected void warnBecauseOfUnsavedData() { 546 int numProblems = model.getNumCancel() + model.getNumFailed(); 547 if (numProblems == 0) return; 548 String msg = trn( 549 "<html>An upload and/or save operation of one layer with modifications<br>" 550 + "was canceled or has failed.</html>", 551 "<html>Upload and/or save operations of {0} layers with modifications<br>" 552 + "were canceled or have failed.</html>", 553 numProblems, 554 numProblems 555 ); 556 JOptionPane.showMessageDialog( 557 Main.parent, 558 msg, 559 tr("Incomplete upload and/or save"), 560 JOptionPane.WARNING_MESSAGE 561 ); 562 } 563 564 @Override 565 public void run() { 566 GuiHelper.runInEDTAndWait(new Runnable() { 567 @Override 568 public void run() { 569 model.setMode(SaveLayersModel.Mode.UPLOADING_AND_SAVING); 570 List<SaveLayerInfo> toUpload = model.getLayersToUpload(); 571 if (!toUpload.isEmpty()) { 572 uploadLayers(toUpload); 573 } 574 List<SaveLayerInfo> toSave = model.getLayersToSave(); 575 if (!toSave.isEmpty()) { 576 saveLayers(toSave); 577 } 578 model.setMode(SaveLayersModel.Mode.EDITING_DATA); 579 if (model.hasUnsavedData()) { 580 warnBecauseOfUnsavedData(); 581 model.setMode(Mode.EDITING_DATA); 582 if (canceled) { 583 setUserAction(UserAction.CANCEL); 584 closeDialog(); 585 } 586 } else { 587 setUserAction(UserAction.PROCEED); 588 closeDialog(); 589 } 590 } 591 }); 592 } 593 594 public void cancel() { 595 if (currentTask != null) { 596 currentTask.cancel(); 597 } 598 canceled = true; 599 } 600 } 601 602 @Override 603 public void tableChanged(TableModelEvent arg0) { 604 boolean dis = model.getLayersToSave().isEmpty() && model.getLayersToUpload().isEmpty(); 605 if(saveAndProceedActionButton != null) { 606 saveAndProceedActionButton.setEnabled(!dis); 607 } 608 saveAndProceedAction.redrawIcon(); 609 } 610}