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