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 static final class LayerCellRenderer implements ListCellRenderer<SaveLayerInfo> { 228 private final DefaultListCellRenderer def = new DefaultListCellRenderer(); 229 230 @Override 231 public Component getListCellRendererComponent(JList<? extends SaveLayerInfo> list, SaveLayerInfo info, int index, 232 boolean isSelected, boolean cellHasFocus) { 233 def.setIcon(info.getLayer().getIcon()); 234 def.setText(info.getName()); 235 return def; 236 } 237 } 238 239 private final JLabel lblMessage = new JLabel(); 240 private final JList<SaveLayerInfo> lstLayers = new JList<>(); 241 242 LayerListWarningMessagePanel(String msg, List<SaveLayerInfo> infos) { 243 super(new GridBagLayout()); 244 build(); 245 lblMessage.setText(msg); 246 lstLayers.setListData(infos.toArray(new SaveLayerInfo[infos.size()])); 247 } 248 249 protected void build() { 250 GridBagConstraints gc = new GridBagConstraints(); 251 gc.gridx = 0; 252 gc.gridy = 0; 253 gc.fill = GridBagConstraints.HORIZONTAL; 254 gc.weightx = 1.0; 255 gc.weighty = 0.0; 256 add(lblMessage, gc); 257 lblMessage.setHorizontalAlignment(JLabel.LEFT); 258 lstLayers.setCellRenderer(new LayerCellRenderer()); 259 gc.gridx = 0; 260 gc.gridy = 1; 261 gc.fill = GridBagConstraints.HORIZONTAL; 262 gc.weightx = 1.0; 263 gc.weighty = 1.0; 264 add(lstLayers, gc); 265 } 266 } 267 268 private static void warn(String msg, List<SaveLayerInfo> infos, String title) { 269 JPanel panel = new LayerListWarningMessagePanel(msg, infos); 270 // For unit test coverage in headless mode 271 if (!GraphicsEnvironment.isHeadless()) { 272 JOptionPane.showConfirmDialog(Main.parent, panel, title, JOptionPane.DEFAULT_OPTION, JOptionPane.WARNING_MESSAGE); 273 } 274 } 275 276 protected static void warnLayersWithConflictsAndUploadRequest(List<SaveLayerInfo> infos) { 277 warn(trn("<html>{0} layer has unresolved conflicts.<br>" 278 + "Either resolve them first or discard the modifications.<br>" 279 + "Layer with conflicts:</html>", 280 "<html>{0} layers have unresolved conflicts.<br>" 281 + "Either resolve them first or discard the modifications.<br>" 282 + "Layers with conflicts:</html>", 283 infos.size(), 284 infos.size()), 285 infos, tr("Unsaved data and conflicts")); 286 } 287 288 protected static void warnLayersWithoutFilesAndSaveRequest(List<SaveLayerInfo> infos) { 289 warn(trn("<html>{0} layer needs saving but has no associated file.<br>" 290 + "Either select a file for this layer or discard the changes.<br>" 291 + "Layer without a file:</html>", 292 "<html>{0} layers need saving but have no associated file.<br>" 293 + "Either select a file for each of them or discard the changes.<br>" 294 + "Layers without a file:</html>", 295 infos.size(), 296 infos.size()), 297 infos, tr("Unsaved data and missing associated file")); 298 } 299 300 protected static void warnLayersWithIllegalFilesAndSaveRequest(List<SaveLayerInfo> infos) { 301 warn(trn("<html>{0} layer needs saving but has an associated file<br>" 302 + "which cannot be written.<br>" 303 + "Either select another file for this layer or discard the changes.<br>" 304 + "Layer with a non-writable file:</html>", 305 "<html>{0} layers need saving but have associated files<br>" 306 + "which cannot be written.<br>" 307 + "Either select another file for each of them or discard the changes.<br>" 308 + "Layers with non-writable files:</html>", 309 infos.size(), 310 infos.size()), 311 infos, tr("Unsaved data non-writable files")); 312 } 313 314 static boolean confirmSaveLayerInfosOK(SaveLayersModel model) { 315 List<SaveLayerInfo> layerInfos = model.getLayersWithConflictsAndUploadRequest(); 316 if (!layerInfos.isEmpty()) { 317 warnLayersWithConflictsAndUploadRequest(layerInfos); 318 return false; 319 } 320 321 layerInfos = model.getLayersWithoutFilesAndSaveRequest(); 322 if (!layerInfos.isEmpty()) { 323 warnLayersWithoutFilesAndSaveRequest(layerInfos); 324 return false; 325 } 326 327 layerInfos = model.getLayersWithIllegalFilesAndSaveRequest(); 328 if (!layerInfos.isEmpty()) { 329 warnLayersWithIllegalFilesAndSaveRequest(layerInfos); 330 return false; 331 } 332 333 return true; 334 } 335 336 protected void setUserAction(UserAction action) { 337 this.action = action; 338 } 339 340 /** 341 * Closes this dialog and frees all native screen resources. 342 */ 343 public void closeDialog() { 344 setVisible(false); 345 dispose(); 346 } 347 348 class WindowClosingAdapter extends WindowAdapter { 349 @Override 350 public void windowClosing(WindowEvent e) { 351 cancelAction.cancel(); 352 } 353 } 354 355 class CancelAction extends AbstractAction { 356 CancelAction() { 357 putValue(NAME, tr("Cancel")); 358 putValue(SHORT_DESCRIPTION, tr("Close this dialog and resume editing in JOSM")); 359 putValue(SMALL_ICON, ImageProvider.get("cancel")); 360 InputMapUtils.addEscapeAction(getRootPane(), this); 361 } 362 363 protected void cancelWhenInEditingModel() { 364 setUserAction(UserAction.CANCEL); 365 closeDialog(); 366 } 367 368 public void cancel() { 369 switch(model.getMode()) { 370 case EDITING_DATA: cancelWhenInEditingModel(); 371 break; 372 case UPLOADING_AND_SAVING: cancelSafeAndUploadTask(); 373 break; 374 } 375 } 376 377 @Override 378 public void actionPerformed(ActionEvent e) { 379 cancel(); 380 } 381 } 382 383 class DiscardAndProceedAction extends AbstractAction implements PropertyChangeListener { 384 DiscardAndProceedAction() { 385 initForReason(Reason.EXIT); 386 } 387 388 public void initForReason(Reason reason) { 389 switch (reason) { 390 case EXIT: 391 putValue(NAME, tr("Exit now!")); 392 putValue(SHORT_DESCRIPTION, tr("Exit JOSM without saving. Unsaved changes are lost.")); 393 putValue(SMALL_ICON, ImageProvider.get("exit")); 394 break; 395 case RESTART: 396 putValue(NAME, tr("Restart now!")); 397 putValue(SHORT_DESCRIPTION, tr("Restart JOSM without saving. Unsaved changes are lost.")); 398 putValue(SMALL_ICON, ImageProvider.get("restart")); 399 break; 400 case DELETE: 401 putValue(NAME, tr("Delete now!")); 402 putValue(SHORT_DESCRIPTION, tr("Delete layers without saving. Unsaved changes are lost.")); 403 putValue(SMALL_ICON, ImageProvider.get("dialogs", "delete")); 404 break; 405 } 406 407 } 408 409 @Override 410 public void actionPerformed(ActionEvent e) { 411 setUserAction(UserAction.PROCEED); 412 closeDialog(); 413 } 414 415 @Override 416 public void propertyChange(PropertyChangeEvent evt) { 417 if (evt.getPropertyName().equals(SaveLayersModel.MODE_PROP)) { 418 Mode mode = (Mode) evt.getNewValue(); 419 switch(mode) { 420 case EDITING_DATA: setEnabled(true); 421 break; 422 case UPLOADING_AND_SAVING: setEnabled(false); 423 break; 424 } 425 } 426 } 427 } 428 429 class SaveSessionAction extends SessionSaveAsAction { 430 431 SaveSessionAction() { 432 super(false, false); 433 } 434 435 @Override 436 public void actionPerformed(ActionEvent e) { 437 try { 438 saveSession(); 439 setUserAction(UserAction.PROCEED); 440 closeDialog(); 441 } catch (UserCancelException ignore) { 442 Main.trace(ignore); 443 } 444 } 445 } 446 447 final class SaveAndProceedAction extends AbstractAction implements PropertyChangeListener { 448 private static final int ICON_SIZE = 24; 449 private static final String BASE_ICON = "BASE_ICON"; 450 private final transient Image save = getImage("save", false); 451 private final transient Image upld = getImage("upload", false); 452 private final transient Image saveDis = getImage("save", true); 453 private final transient Image upldDis = getImage("upload", true); 454 455 SaveAndProceedAction() { 456 initForReason(Reason.EXIT); 457 } 458 459 Image getImage(String name, boolean disabled) { 460 ImageIcon img = new ImageProvider(name).setDisabled(disabled).get(); 461 return img != null ? img.getImage() : null; 462 } 463 464 public void initForReason(Reason reason) { 465 switch (reason) { 466 case EXIT: 467 putValue(NAME, tr("Perform actions before exiting")); 468 putValue(SHORT_DESCRIPTION, tr("Exit JOSM with saving. Unsaved changes are uploaded and/or saved.")); 469 putValue(BASE_ICON, ImageProvider.get("exit")); 470 break; 471 case RESTART: 472 putValue(NAME, tr("Perform actions before restarting")); 473 putValue(SHORT_DESCRIPTION, tr("Restart JOSM with saving. Unsaved changes are uploaded and/or saved.")); 474 putValue(BASE_ICON, ImageProvider.get("restart")); 475 break; 476 case DELETE: 477 putValue(NAME, tr("Perform actions before deleting")); 478 putValue(SHORT_DESCRIPTION, tr("Save/Upload layers before deleting. Unsaved changes are not lost.")); 479 putValue(BASE_ICON, ImageProvider.get("dialogs", "delete")); 480 break; 481 } 482 redrawIcon(); 483 } 484 485 public void redrawIcon() { 486 Image base = ((ImageIcon) getValue(BASE_ICON)).getImage(); 487 BufferedImage newIco = new BufferedImage(ICON_SIZE*3, ICON_SIZE, BufferedImage.TYPE_4BYTE_ABGR); 488 Graphics2D g = newIco.createGraphics(); 489 // CHECKSTYLE.OFF: SingleSpaceSeparator 490 g.drawImage(model.getLayersToUpload().isEmpty() ? upldDis : upld, ICON_SIZE*0, 0, ICON_SIZE, ICON_SIZE, null); 491 g.drawImage(model.getLayersToSave().isEmpty() ? saveDis : save, ICON_SIZE*1, 0, ICON_SIZE, ICON_SIZE, null); 492 g.drawImage(base, ICON_SIZE*2, 0, ICON_SIZE, ICON_SIZE, null); 493 // CHECKSTYLE.ON: SingleSpaceSeparator 494 putValue(SMALL_ICON, new ImageIcon(newIco)); 495 } 496 497 @Override 498 public void actionPerformed(ActionEvent e) { 499 if (!confirmSaveLayerInfosOK(model)) 500 return; 501 launchSafeAndUploadTask(); 502 } 503 504 @Override 505 public void propertyChange(PropertyChangeEvent evt) { 506 if (evt.getPropertyName().equals(SaveLayersModel.MODE_PROP)) { 507 SaveLayersModel.Mode mode = (SaveLayersModel.Mode) evt.getNewValue(); 508 switch(mode) { 509 case EDITING_DATA: setEnabled(true); 510 break; 511 case UPLOADING_AND_SAVING: setEnabled(false); 512 break; 513 } 514 } 515 } 516 } 517 518 /** 519 * This is the asynchronous task which uploads modified layers to the server and 520 * saves them to files, if requested by the user. 521 * 522 */ 523 protected class SaveAndUploadTask implements Runnable { 524 525 private final SaveLayersModel model; 526 private final ProgressMonitor monitor; 527 private final ExecutorService worker; 528 private boolean canceled; 529 private Future<?> currentFuture; 530 private AbstractIOTask currentTask; 531 532 public SaveAndUploadTask(SaveLayersModel model, ProgressMonitor monitor) { 533 this.model = model; 534 this.monitor = monitor; 535 this.worker = Executors.newSingleThreadExecutor(Utils.newThreadFactory(getClass() + "-%d", Thread.NORM_PRIORITY)); 536 } 537 538 protected void uploadLayers(List<SaveLayerInfo> toUpload) { 539 for (final SaveLayerInfo layerInfo: toUpload) { 540 AbstractModifiableLayer layer = layerInfo.getLayer(); 541 if (canceled) { 542 model.setUploadState(layer, UploadOrSaveState.CANCELED); 543 continue; 544 } 545 monitor.subTask(tr("Preparing layer ''{0}'' for upload ...", layerInfo.getName())); 546 547 if (!UploadAction.checkPreUploadConditions(layer)) { 548 model.setUploadState(layer, UploadOrSaveState.FAILED); 549 continue; 550 } 551 552 AbstractUploadDialog dialog = layer.getUploadDialog(); 553 if (dialog != null) { 554 dialog.setVisible(true); 555 if (dialog.isCanceled()) { 556 model.setUploadState(layer, UploadOrSaveState.CANCELED); 557 continue; 558 } 559 dialog.rememberUserInput(); 560 } 561 562 currentTask = layer.createUploadTask(monitor); 563 if (currentTask == null) { 564 model.setUploadState(layer, UploadOrSaveState.FAILED); 565 continue; 566 } 567 currentFuture = worker.submit(currentTask); 568 try { 569 // wait for the asynchronous task to complete 570 // 571 currentFuture.get(); 572 } catch (CancellationException e) { 573 Main.trace(e); 574 model.setUploadState(layer, UploadOrSaveState.CANCELED); 575 } catch (InterruptedException | ExecutionException e) { 576 Main.error(e); 577 model.setUploadState(layer, UploadOrSaveState.FAILED); 578 ExceptionDialogUtil.explainException(e); 579 } 580 if (currentTask.isCanceled()) { 581 model.setUploadState(layer, UploadOrSaveState.CANCELED); 582 } else if (currentTask.isFailed()) { 583 Main.error(currentTask.getLastException()); 584 ExceptionDialogUtil.explainException(currentTask.getLastException()); 585 model.setUploadState(layer, UploadOrSaveState.FAILED); 586 } else { 587 model.setUploadState(layer, UploadOrSaveState.OK); 588 } 589 currentTask = null; 590 currentFuture = null; 591 } 592 } 593 594 protected void saveLayers(List<SaveLayerInfo> toSave) { 595 for (final SaveLayerInfo layerInfo: toSave) { 596 if (canceled) { 597 model.setSaveState(layerInfo.getLayer(), UploadOrSaveState.CANCELED); 598 continue; 599 } 600 // Check save preconditions earlier to avoid a blocking reentring call to EDT (see #10086) 601 if (layerInfo.isDoCheckSaveConditions()) { 602 if (!layerInfo.getLayer().checkSaveConditions()) { 603 continue; 604 } 605 layerInfo.setDoCheckSaveConditions(false); 606 } 607 currentTask = new SaveLayerTask(layerInfo, monitor); 608 currentFuture = worker.submit(currentTask); 609 610 try { 611 // wait for the asynchronous task to complete 612 // 613 currentFuture.get(); 614 } catch (CancellationException e) { 615 Main.trace(e); 616 model.setSaveState(layerInfo.getLayer(), UploadOrSaveState.CANCELED); 617 } catch (InterruptedException | ExecutionException e) { 618 Main.error(e); 619 model.setSaveState(layerInfo.getLayer(), UploadOrSaveState.FAILED); 620 ExceptionDialogUtil.explainException(e); 621 } 622 if (currentTask.isCanceled()) { 623 model.setSaveState(layerInfo.getLayer(), UploadOrSaveState.CANCELED); 624 } else if (currentTask.isFailed()) { 625 if (currentTask.getLastException() != null) { 626 Main.error(currentTask.getLastException()); 627 ExceptionDialogUtil.explainException(currentTask.getLastException()); 628 } 629 model.setSaveState(layerInfo.getLayer(), UploadOrSaveState.FAILED); 630 } else { 631 model.setSaveState(layerInfo.getLayer(), UploadOrSaveState.OK); 632 } 633 this.currentTask = null; 634 this.currentFuture = null; 635 } 636 } 637 638 protected void warnBecauseOfUnsavedData() { 639 int numProblems = model.getNumCancel() + model.getNumFailed(); 640 if (numProblems == 0) 641 return; 642 Main.warn(numProblems + " problems occured during upload/save"); 643 String msg = trn( 644 "<html>An upload and/or save operation of one layer with modifications<br>" 645 + "was canceled or has failed.</html>", 646 "<html>Upload and/or save operations of {0} layers with modifications<br>" 647 + "were canceled or have failed.</html>", 648 numProblems, 649 numProblems 650 ); 651 JOptionPane.showMessageDialog( 652 Main.parent, 653 msg, 654 tr("Incomplete upload and/or save"), 655 JOptionPane.WARNING_MESSAGE 656 ); 657 } 658 659 @Override 660 public void run() { 661 GuiHelper.runInEDTAndWait(() -> { 662 model.setMode(SaveLayersModel.Mode.UPLOADING_AND_SAVING); 663 List<SaveLayerInfo> toUpload = model.getLayersToUpload(); 664 if (!toUpload.isEmpty()) { 665 uploadLayers(toUpload); 666 } 667 List<SaveLayerInfo> toSave = model.getLayersToSave(); 668 if (!toSave.isEmpty()) { 669 saveLayers(toSave); 670 } 671 model.setMode(SaveLayersModel.Mode.EDITING_DATA); 672 if (model.hasUnsavedData()) { 673 warnBecauseOfUnsavedData(); 674 model.setMode(Mode.EDITING_DATA); 675 if (canceled) { 676 setUserAction(UserAction.CANCEL); 677 closeDialog(); 678 } 679 } else { 680 setUserAction(UserAction.PROCEED); 681 closeDialog(); 682 } 683 }); 684 worker.shutdownNow(); 685 } 686 687 public void cancel() { 688 if (currentTask != null) { 689 currentTask.cancel(); 690 } 691 worker.shutdown(); 692 canceled = true; 693 } 694 } 695 696 @Override 697 public void tableChanged(TableModelEvent arg0) { 698 boolean dis = model.getLayersToSave().isEmpty() && model.getLayersToUpload().isEmpty(); 699 if (saveAndProceedActionButton != null) { 700 saveAndProceedActionButton.setEnabled(!dis); 701 } 702 saveAndProceedAction.redrawIcon(); 703 } 704}