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