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