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.Graphics2D; 011import java.awt.GraphicsEnvironment; 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.ArrayList; 022import java.util.List; 023import java.util.concurrent.CancellationException; 024import java.util.concurrent.ExecutionException; 025import java.util.concurrent.ExecutorService; 026import java.util.concurrent.Executors; 027import java.util.concurrent.Future; 028 029import javax.swing.AbstractAction; 030import javax.swing.DefaultListCellRenderer; 031import javax.swing.ImageIcon; 032import javax.swing.JButton; 033import javax.swing.JDialog; 034import javax.swing.JLabel; 035import javax.swing.JList; 036import javax.swing.JOptionPane; 037import javax.swing.JPanel; 038import javax.swing.JScrollPane; 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.SessionSaveAsAction; 046import org.openstreetmap.josm.actions.UploadAction; 047import org.openstreetmap.josm.gui.ExceptionDialogUtil; 048import org.openstreetmap.josm.gui.io.SaveLayersModel.Mode; 049import org.openstreetmap.josm.gui.layer.AbstractModifiableLayer; 050import org.openstreetmap.josm.gui.layer.Layer; 051import org.openstreetmap.josm.gui.progress.ProgressMonitor; 052import org.openstreetmap.josm.gui.progress.SwingRenderingProgressMonitor; 053import org.openstreetmap.josm.gui.util.GuiHelper; 054import org.openstreetmap.josm.tools.GBC; 055import org.openstreetmap.josm.tools.ImageProvider; 056import org.openstreetmap.josm.tools.InputMapUtils; 057import org.openstreetmap.josm.tools.UserCancelException; 058import org.openstreetmap.josm.tools.Utils; 059import org.openstreetmap.josm.tools.WindowGeometry; 060 061public class SaveLayersDialog extends JDialog implements TableModelListener { 062 063 /** 064 * The cause for requesting an action on unsaved modifications 065 */ 066 public enum Reason { 067 /** deleting a layer */ 068 DELETE, 069 /** exiting JOSM */ 070 EXIT, 071 /* restarting JOSM */ 072 RESTART 073 } 074 075 private enum UserAction { 076 /** save/upload layers was successful, proceed with operation */ 077 PROCEED, 078 /** save/upload of layers was not successful or user canceled operation */ 079 CANCEL 080 } 081 082 private final SaveLayersModel model = new SaveLayersModel(); 083 private UserAction action = UserAction.CANCEL; 084 private final UploadAndSaveProgressRenderer pnlUploadLayers = new UploadAndSaveProgressRenderer(); 085 086 private final SaveAndProceedAction saveAndProceedAction = new SaveAndProceedAction(); 087 private final SaveSessionAction saveSessionAction = new SaveSessionAction(); 088 private final DiscardAndProceedAction discardAndProceedAction = new DiscardAndProceedAction(); 089 private final CancelAction cancelAction = new CancelAction(); 090 private transient SaveAndUploadTask saveAndUploadTask; 091 092 private final JButton saveAndProceedActionButton = new JButton(saveAndProceedAction); 093 094 /** 095 * Asks user to perform "save layer" operations (save on disk and/or upload data to server) before data layers deletion. 096 * 097 * @param selectedLayers The layers to check. Only instances of {@link AbstractModifiableLayer} are considered. 098 * @param reason the cause for requesting an action on unsaved modifications 099 * @return {@code true} if there was nothing to save, or if the user wants to proceed to save operations. 100 * {@code false} if the user cancels. 101 * @since 11093 102 */ 103 public static boolean saveUnsavedModifications(Iterable<? extends Layer> selectedLayers, Reason reason) { 104 if (!GraphicsEnvironment.isHeadless()) { 105 SaveLayersDialog dialog = new SaveLayersDialog(Main.parent); 106 List<AbstractModifiableLayer> layersWithUnmodifiedChanges = new ArrayList<>(); 107 for (Layer l: selectedLayers) { 108 if (!(l instanceof AbstractModifiableLayer)) { 109 continue; 110 } 111 AbstractModifiableLayer odl = (AbstractModifiableLayer) l; 112 if (odl.isModified() && 113 ((!odl.isSavable() && !odl.isUploadable()) || 114 odl.requiresSaveToFile() || 115 (odl.requiresUploadToServer() && !odl.isUploadDiscouraged()))) { 116 layersWithUnmodifiedChanges.add(odl); 117 } 118 } 119 dialog.prepareForSavingAndUpdatingLayers(reason); 120 if (!layersWithUnmodifiedChanges.isEmpty()) { 121 dialog.getModel().populate(layersWithUnmodifiedChanges); 122 dialog.setVisible(true); 123 switch(dialog.getUserAction()) { 124 case PROCEED: return true; 125 case CANCEL: 126 default: return false; 127 } 128 } 129 } 130 131 return true; 132 } 133 134 /** 135 * Constructs a new {@code SaveLayersDialog}. 136 * @param parent parent component 137 */ 138 public SaveLayersDialog(Component parent) { 139 super(GuiHelper.getFrameForComponent(parent), ModalityType.DOCUMENT_MODAL); 140 build(); 141 } 142 143 /** 144 * builds the GUI 145 */ 146 protected void build() { 147 WindowGeometry geometry = WindowGeometry.centerOnScreen(new Dimension(650, 300)); 148 geometry.applySafe(this); 149 getContentPane().setLayout(new BorderLayout()); 150 151 SaveLayersTable table = new SaveLayersTable(model); 152 JScrollPane pane = new JScrollPane(table); 153 model.addPropertyChangeListener(table); 154 table.getModel().addTableModelListener(this); 155 156 getContentPane().add(pane, BorderLayout.CENTER); 157 getContentPane().add(buildButtonRow(), BorderLayout.SOUTH); 158 159 addWindowListener(new WindowClosingAdapter()); 160 setDefaultCloseOperation(WindowConstants.DISPOSE_ON_CLOSE); 161 } 162 163 /** 164 * builds the button row 165 * 166 * @return the panel with the button row 167 */ 168 protected JPanel buildButtonRow() { 169 JPanel pnl = new JPanel(new GridBagLayout()); 170 171 model.addPropertyChangeListener(saveAndProceedAction); 172 pnl.add(saveAndProceedActionButton, GBC.std(0, 0).insets(5, 5, 0, 0).fill(GBC.HORIZONTAL)); 173 174 pnl.add(new JButton(saveSessionAction), GBC.std(1, 0).insets(5, 5, 5, 0).fill(GBC.HORIZONTAL)); 175 176 model.addPropertyChangeListener(discardAndProceedAction); 177 pnl.add(new JButton(discardAndProceedAction), GBC.std(0, 1).insets(5, 5, 0, 5).fill(GBC.HORIZONTAL)); 178 179 pnl.add(new JButton(cancelAction), GBC.std(1, 1).insets(5, 5, 5, 5).fill(GBC.HORIZONTAL)); 180 181 JPanel pnl2 = new JPanel(new BorderLayout()); 182 pnl2.add(pnlUploadLayers, BorderLayout.CENTER); 183 model.addPropertyChangeListener(pnlUploadLayers); 184 pnl2.add(pnl, BorderLayout.SOUTH); 185 return pnl2; 186 } 187 188 public void prepareForSavingAndUpdatingLayers(final Reason reason) { 189 switch (reason) { 190 case EXIT: 191 setTitle(tr("Unsaved changes - Save/Upload before exiting?")); 192 break; 193 case DELETE: 194 setTitle(tr("Unsaved changes - Save/Upload before deleting?")); 195 break; 196 case RESTART: 197 setTitle(tr("Unsaved changes - Save/Upload before restarting?")); 198 break; 199 } 200 this.saveAndProceedAction.initForReason(reason); 201 this.discardAndProceedAction.initForReason(reason); 202 } 203 204 public UserAction getUserAction() { 205 return this.action; 206 } 207 208 public SaveLayersModel getModel() { 209 return model; 210 } 211 212 protected void launchSafeAndUploadTask() { 213 ProgressMonitor monitor = new SwingRenderingProgressMonitor(pnlUploadLayers); 214 monitor.beginTask(tr("Uploading and saving modified layers ...")); 215 this.saveAndUploadTask = new SaveAndUploadTask(model, monitor); 216 new Thread(saveAndUploadTask, saveAndUploadTask.getClass().getName()).start(); 217 } 218 219 protected void cancelSafeAndUploadTask() { 220 if (this.saveAndUploadTask != null) { 221 this.saveAndUploadTask.cancel(); 222 } 223 model.setMode(Mode.EDITING_DATA); 224 } 225 226 private static class LayerListWarningMessagePanel extends JPanel { 227 private final JLabel lblMessage = new JLabel(); 228 private final JList<SaveLayerInfo> lstLayers = new JList<>(); 229 230 LayerListWarningMessagePanel(String msg, List<SaveLayerInfo> infos) { 231 build(); 232 lblMessage.setText(msg); 233 lstLayers.setListData(infos.toArray(new SaveLayerInfo[infos.size()])); 234 } 235 236 protected void build() { 237 setLayout(new GridBagLayout()); 238 GridBagConstraints gc = new GridBagConstraints(); 239 gc.gridx = 0; 240 gc.gridy = 0; 241 gc.fill = GridBagConstraints.HORIZONTAL; 242 gc.weightx = 1.0; 243 gc.weighty = 0.0; 244 add(lblMessage, gc); 245 lblMessage.setHorizontalAlignment(JLabel.LEFT); 246 lstLayers.setCellRenderer( 247 new ListCellRenderer<SaveLayerInfo>() { 248 private final DefaultListCellRenderer def = new DefaultListCellRenderer(); 249 @Override 250 public Component getListCellRendererComponent(JList<? extends SaveLayerInfo> list, SaveLayerInfo info, int index, 251 boolean isSelected, boolean cellHasFocus) { 252 def.setIcon(info.getLayer().getIcon()); 253 def.setText(info.getName()); 254 return def; 255 } 256 } 257 ); 258 gc.gridx = 0; 259 gc.gridy = 1; 260 gc.fill = GridBagConstraints.HORIZONTAL; 261 gc.weightx = 1.0; 262 gc.weighty = 1.0; 263 add(lstLayers, gc); 264 } 265 } 266 267 private static void warn(String msg, List<SaveLayerInfo> infos, String title) { 268 JPanel panel = new LayerListWarningMessagePanel(msg, infos); 269 // For unit test coverage in headless mode 270 if (!GraphicsEnvironment.isHeadless()) { 271 JOptionPane.showConfirmDialog(Main.parent, panel, title, JOptionPane.DEFAULT_OPTION, JOptionPane.WARNING_MESSAGE); 272 } 273 } 274 275 protected static void warnLayersWithConflictsAndUploadRequest(List<SaveLayerInfo> infos) { 276 warn(trn("<html>{0} layer has unresolved conflicts.<br>" 277 + "Either resolve them first or discard the modifications.<br>" 278 + "Layer with conflicts:</html>", 279 "<html>{0} layers have unresolved conflicts.<br>" 280 + "Either resolve them first or discard the modifications.<br>" 281 + "Layers with conflicts:</html>", 282 infos.size(), 283 infos.size()), 284 infos, tr("Unsaved data and conflicts")); 285 } 286 287 protected static void warnLayersWithoutFilesAndSaveRequest(List<SaveLayerInfo> infos) { 288 warn(trn("<html>{0} layer needs saving but has no associated file.<br>" 289 + "Either select a file for this layer or discard the changes.<br>" 290 + "Layer without a file:</html>", 291 "<html>{0} layers need saving but have no associated file.<br>" 292 + "Either select a file for each of them or discard the changes.<br>" 293 + "Layers without a file:</html>", 294 infos.size(), 295 infos.size()), 296 infos, tr("Unsaved data and missing associated file")); 297 } 298 299 protected static void warnLayersWithIllegalFilesAndSaveRequest(List<SaveLayerInfo> infos) { 300 warn(trn("<html>{0} layer needs saving but has an associated file<br>" 301 + "which cannot be written.<br>" 302 + "Either select another file for this layer or discard the changes.<br>" 303 + "Layer with a non-writable file:</html>", 304 "<html>{0} layers need saving but have associated files<br>" 305 + "which cannot be written.<br>" 306 + "Either select another file for each of them or discard the changes.<br>" 307 + "Layers with non-writable files:</html>", 308 infos.size(), 309 infos.size()), 310 infos, tr("Unsaved data non-writable files")); 311 } 312 313 static boolean confirmSaveLayerInfosOK(SaveLayersModel model) { 314 List<SaveLayerInfo> layerInfos = model.getLayersWithConflictsAndUploadRequest(); 315 if (!layerInfos.isEmpty()) { 316 warnLayersWithConflictsAndUploadRequest(layerInfos); 317 return false; 318 } 319 320 layerInfos = model.getLayersWithoutFilesAndSaveRequest(); 321 if (!layerInfos.isEmpty()) { 322 warnLayersWithoutFilesAndSaveRequest(layerInfos); 323 return false; 324 } 325 326 layerInfos = model.getLayersWithIllegalFilesAndSaveRequest(); 327 if (!layerInfos.isEmpty()) { 328 warnLayersWithIllegalFilesAndSaveRequest(layerInfos); 329 return false; 330 } 331 332 return true; 333 } 334 335 protected void setUserAction(UserAction action) { 336 this.action = action; 337 } 338 339 /** 340 * Closes this dialog and frees all native screen resources. 341 */ 342 public void closeDialog() { 343 setVisible(false); 344 dispose(); 345 } 346 347 class WindowClosingAdapter extends WindowAdapter { 348 @Override 349 public void windowClosing(WindowEvent e) { 350 cancelAction.cancel(); 351 } 352 } 353 354 class CancelAction extends AbstractAction { 355 CancelAction() { 356 putValue(NAME, tr("Cancel")); 357 putValue(SHORT_DESCRIPTION, tr("Close this dialog and resume editing in JOSM")); 358 putValue(SMALL_ICON, ImageProvider.get("cancel")); 359 InputMapUtils.addEscapeAction(getRootPane(), this); 360 } 361 362 protected void cancelWhenInEditingModel() { 363 setUserAction(UserAction.CANCEL); 364 closeDialog(); 365 } 366 367 public void cancel() { 368 switch(model.getMode()) { 369 case EDITING_DATA: cancelWhenInEditingModel(); 370 break; 371 case UPLOADING_AND_SAVING: cancelSafeAndUploadTask(); 372 break; 373 } 374 } 375 376 @Override 377 public void actionPerformed(ActionEvent e) { 378 cancel(); 379 } 380 } 381 382 class DiscardAndProceedAction extends AbstractAction implements PropertyChangeListener { 383 DiscardAndProceedAction() { 384 initForReason(Reason.EXIT); 385 } 386 387 public void initForReason(Reason reason) { 388 switch (reason) { 389 case EXIT: 390 putValue(NAME, tr("Exit now!")); 391 putValue(SHORT_DESCRIPTION, tr("Exit JOSM without saving. Unsaved changes are lost.")); 392 putValue(SMALL_ICON, ImageProvider.get("exit")); 393 break; 394 case RESTART: 395 putValue(NAME, tr("Restart now!")); 396 putValue(SHORT_DESCRIPTION, tr("Restart JOSM without saving. Unsaved changes are lost.")); 397 putValue(SMALL_ICON, ImageProvider.get("restart")); 398 break; 399 case DELETE: 400 putValue(NAME, tr("Delete now!")); 401 putValue(SHORT_DESCRIPTION, tr("Delete layers without saving. Unsaved changes are lost.")); 402 putValue(SMALL_ICON, ImageProvider.get("dialogs", "delete")); 403 break; 404 } 405 406 } 407 408 @Override 409 public void actionPerformed(ActionEvent e) { 410 setUserAction(UserAction.PROCEED); 411 closeDialog(); 412 } 413 414 @Override 415 public void propertyChange(PropertyChangeEvent evt) { 416 if (evt.getPropertyName().equals(SaveLayersModel.MODE_PROP)) { 417 Mode mode = (Mode) evt.getNewValue(); 418 switch(mode) { 419 case EDITING_DATA: setEnabled(true); 420 break; 421 case UPLOADING_AND_SAVING: setEnabled(false); 422 break; 423 } 424 } 425 } 426 } 427 428 class SaveSessionAction extends SessionSaveAsAction { 429 430 SaveSessionAction() { 431 super(false, false); 432 } 433 434 @Override 435 public void actionPerformed(ActionEvent e) { 436 try { 437 saveSession(); 438 setUserAction(UserAction.PROCEED); 439 closeDialog(); 440 } catch (UserCancelException ignore) { 441 Main.trace(ignore); 442 } 443 } 444 } 445 446 final class SaveAndProceedAction extends AbstractAction implements PropertyChangeListener { 447 private static final int ICON_SIZE = 24; 448 private static final String BASE_ICON = "BASE_ICON"; 449 private final transient Image save = ImageProvider.get("save").getImage(); 450 private final transient Image upld = ImageProvider.get("upload").getImage(); 451 private final transient Image saveDis = new ImageProvider("save").setDisabled(true).get().getImage(); 452 private final transient Image upldDis = new ImageProvider("upload").setDisabled(true).get().getImage(); 453 454 SaveAndProceedAction() { 455 initForReason(Reason.EXIT); 456 } 457 458 public void initForReason(Reason reason) { 459 switch (reason) { 460 case EXIT: 461 putValue(NAME, tr("Perform actions before exiting")); 462 putValue(SHORT_DESCRIPTION, tr("Exit JOSM with saving. Unsaved changes are uploaded and/or saved.")); 463 putValue(BASE_ICON, ImageProvider.get("exit")); 464 break; 465 case RESTART: 466 putValue(NAME, tr("Perform actions before restarting")); 467 putValue(SHORT_DESCRIPTION, tr("Restart JOSM with saving. Unsaved changes are uploaded and/or saved.")); 468 putValue(BASE_ICON, ImageProvider.get("restart")); 469 break; 470 case DELETE: 471 putValue(NAME, tr("Perform actions before deleting")); 472 putValue(SHORT_DESCRIPTION, tr("Save/Upload layers before deleting. Unsaved changes are not lost.")); 473 putValue(BASE_ICON, ImageProvider.get("dialogs", "delete")); 474 break; 475 } 476 redrawIcon(); 477 } 478 479 public void redrawIcon() { 480 Image base = ((ImageIcon) getValue(BASE_ICON)).getImage(); 481 BufferedImage newIco = new BufferedImage(ICON_SIZE*3, ICON_SIZE, BufferedImage.TYPE_4BYTE_ABGR); 482 Graphics2D g = newIco.createGraphics(); 483 // CHECKSTYLE.OFF: SingleSpaceSeparator 484 g.drawImage(model.getLayersToUpload().isEmpty() ? upldDis : upld, ICON_SIZE*0, 0, ICON_SIZE, ICON_SIZE, null); 485 g.drawImage(model.getLayersToSave().isEmpty() ? saveDis : save, ICON_SIZE*1, 0, ICON_SIZE, ICON_SIZE, null); 486 g.drawImage(base, ICON_SIZE*2, 0, ICON_SIZE, ICON_SIZE, null); 487 // CHECKSTYLE.ON: SingleSpaceSeparator 488 putValue(SMALL_ICON, new ImageIcon(newIco)); 489 } 490 491 @Override 492 public void actionPerformed(ActionEvent e) { 493 if (!confirmSaveLayerInfosOK(model)) 494 return; 495 launchSafeAndUploadTask(); 496 } 497 498 @Override 499 public void propertyChange(PropertyChangeEvent evt) { 500 if (evt.getPropertyName().equals(SaveLayersModel.MODE_PROP)) { 501 SaveLayersModel.Mode mode = (SaveLayersModel.Mode) evt.getNewValue(); 502 switch(mode) { 503 case EDITING_DATA: setEnabled(true); 504 break; 505 case UPLOADING_AND_SAVING: setEnabled(false); 506 break; 507 } 508 } 509 } 510 } 511 512 /** 513 * This is the asynchronous task which uploads modified layers to the server and 514 * saves them to files, if requested by the user. 515 * 516 */ 517 protected class SaveAndUploadTask implements Runnable { 518 519 private final SaveLayersModel model; 520 private final ProgressMonitor monitor; 521 private final ExecutorService worker; 522 private boolean canceled; 523 private Future<?> currentFuture; 524 private AbstractIOTask currentTask; 525 526 public SaveAndUploadTask(SaveLayersModel model, ProgressMonitor monitor) { 527 this.model = model; 528 this.monitor = monitor; 529 this.worker = Executors.newSingleThreadExecutor(Utils.newThreadFactory(getClass() + "-%d", Thread.NORM_PRIORITY)); 530 } 531 532 protected void uploadLayers(List<SaveLayerInfo> toUpload) { 533 for (final SaveLayerInfo layerInfo: toUpload) { 534 AbstractModifiableLayer layer = layerInfo.getLayer(); 535 if (canceled) { 536 model.setUploadState(layer, UploadOrSaveState.CANCELED); 537 continue; 538 } 539 monitor.subTask(tr("Preparing layer ''{0}'' for upload ...", layerInfo.getName())); 540 541 if (!UploadAction.checkPreUploadConditions(layer)) { 542 model.setUploadState(layer, UploadOrSaveState.FAILED); 543 continue; 544 } 545 546 AbstractUploadDialog dialog = layer.getUploadDialog(); 547 if (dialog != null) { 548 dialog.setVisible(true); 549 if (dialog.isCanceled()) { 550 model.setUploadState(layer, UploadOrSaveState.CANCELED); 551 continue; 552 } 553 dialog.rememberUserInput(); 554 } 555 556 currentTask = layer.createUploadTask(monitor); 557 if (currentTask == null) { 558 model.setUploadState(layer, UploadOrSaveState.FAILED); 559 continue; 560 } 561 currentFuture = worker.submit(currentTask); 562 try { 563 // wait for the asynchronous task to complete 564 // 565 currentFuture.get(); 566 } catch (CancellationException e) { 567 Main.trace(e); 568 model.setUploadState(layer, UploadOrSaveState.CANCELED); 569 } catch (InterruptedException | ExecutionException e) { 570 Main.error(e); 571 model.setUploadState(layer, UploadOrSaveState.FAILED); 572 ExceptionDialogUtil.explainException(e); 573 } 574 if (currentTask.isCanceled()) { 575 model.setUploadState(layer, UploadOrSaveState.CANCELED); 576 } else if (currentTask.isFailed()) { 577 Main.error(currentTask.getLastException()); 578 ExceptionDialogUtil.explainException(currentTask.getLastException()); 579 model.setUploadState(layer, UploadOrSaveState.FAILED); 580 } else { 581 model.setUploadState(layer, UploadOrSaveState.OK); 582 } 583 currentTask = null; 584 currentFuture = null; 585 } 586 } 587 588 protected void saveLayers(List<SaveLayerInfo> toSave) { 589 for (final SaveLayerInfo layerInfo: toSave) { 590 if (canceled) { 591 model.setSaveState(layerInfo.getLayer(), UploadOrSaveState.CANCELED); 592 continue; 593 } 594 // Check save preconditions earlier to avoid a blocking reentring call to EDT (see #10086) 595 if (layerInfo.isDoCheckSaveConditions()) { 596 if (!layerInfo.getLayer().checkSaveConditions()) { 597 continue; 598 } 599 layerInfo.setDoCheckSaveConditions(false); 600 } 601 currentTask = new SaveLayerTask(layerInfo, monitor); 602 currentFuture = worker.submit(currentTask); 603 604 try { 605 // wait for the asynchronous task to complete 606 // 607 currentFuture.get(); 608 } catch (CancellationException e) { 609 Main.trace(e); 610 model.setSaveState(layerInfo.getLayer(), UploadOrSaveState.CANCELED); 611 } catch (InterruptedException | ExecutionException e) { 612 Main.error(e); 613 model.setSaveState(layerInfo.getLayer(), UploadOrSaveState.FAILED); 614 ExceptionDialogUtil.explainException(e); 615 } 616 if (currentTask.isCanceled()) { 617 model.setSaveState(layerInfo.getLayer(), UploadOrSaveState.CANCELED); 618 } else if (currentTask.isFailed()) { 619 if (currentTask.getLastException() != null) { 620 Main.error(currentTask.getLastException()); 621 ExceptionDialogUtil.explainException(currentTask.getLastException()); 622 } 623 model.setSaveState(layerInfo.getLayer(), UploadOrSaveState.FAILED); 624 } else { 625 model.setSaveState(layerInfo.getLayer(), UploadOrSaveState.OK); 626 } 627 this.currentTask = null; 628 this.currentFuture = null; 629 } 630 } 631 632 protected void warnBecauseOfUnsavedData() { 633 int numProblems = model.getNumCancel() + model.getNumFailed(); 634 if (numProblems == 0) 635 return; 636 Main.warn(numProblems + " problems occured during upload/save"); 637 String msg = trn( 638 "<html>An upload and/or save operation of one layer with modifications<br>" 639 + "was canceled or has failed.</html>", 640 "<html>Upload and/or save operations of {0} layers with modifications<br>" 641 + "were canceled or have failed.</html>", 642 numProblems, 643 numProblems 644 ); 645 JOptionPane.showMessageDialog( 646 Main.parent, 647 msg, 648 tr("Incomplete upload and/or save"), 649 JOptionPane.WARNING_MESSAGE 650 ); 651 } 652 653 @Override 654 public void run() { 655 GuiHelper.runInEDTAndWait(() -> { 656 model.setMode(SaveLayersModel.Mode.UPLOADING_AND_SAVING); 657 List<SaveLayerInfo> toUpload = model.getLayersToUpload(); 658 if (!toUpload.isEmpty()) { 659 uploadLayers(toUpload); 660 } 661 List<SaveLayerInfo> toSave = model.getLayersToSave(); 662 if (!toSave.isEmpty()) { 663 saveLayers(toSave); 664 } 665 model.setMode(SaveLayersModel.Mode.EDITING_DATA); 666 if (model.hasUnsavedData()) { 667 warnBecauseOfUnsavedData(); 668 model.setMode(Mode.EDITING_DATA); 669 if (canceled) { 670 setUserAction(UserAction.CANCEL); 671 closeDialog(); 672 } 673 } else { 674 setUserAction(UserAction.PROCEED); 675 closeDialog(); 676 } 677 }); 678 worker.shutdownNow(); 679 } 680 681 public void cancel() { 682 if (currentTask != null) { 683 currentTask.cancel(); 684 } 685 worker.shutdown(); 686 canceled = true; 687 } 688 } 689 690 @Override 691 public void tableChanged(TableModelEvent arg0) { 692 boolean dis = model.getLayersToSave().isEmpty() && model.getLayersToUpload().isEmpty(); 693 if (saveAndProceedActionButton != null) { 694 saveAndProceedActionButton.setEnabled(!dis); 695 } 696 saveAndProceedAction.redrawIcon(); 697 } 698}