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.ExecutionException;
024import java.util.concurrent.ExecutorService;
025import java.util.concurrent.Executors;
026import java.util.concurrent.Future;
027
028import javax.swing.AbstractAction;
029import javax.swing.DefaultListCellRenderer;
030import javax.swing.ImageIcon;
031import javax.swing.JButton;
032import javax.swing.JComponent;
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.KeyStroke;
040import javax.swing.ListCellRenderer;
041import javax.swing.WindowConstants;
042import javax.swing.event.TableModelEvent;
043import javax.swing.event.TableModelListener;
044
045import org.openstreetmap.josm.Main;
046import org.openstreetmap.josm.actions.SessionSaveAsAction;
047import org.openstreetmap.josm.actions.UploadAction;
048import org.openstreetmap.josm.gui.ExceptionDialogUtil;
049import org.openstreetmap.josm.gui.io.SaveLayersModel.Mode;
050import org.openstreetmap.josm.gui.layer.AbstractModifiableLayer;
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.UserCancelException;
057import org.openstreetmap.josm.tools.Utils;
058import org.openstreetmap.josm.tools.WindowGeometry;
059
060public class SaveLayersDialog extends JDialog implements TableModelListener {
061    public enum UserAction {
062        /** save/upload layers was successful, proceed with operation */
063        PROCEED,
064        /** save/upload of layers was not successful or user canceled operation */
065        CANCEL
066    }
067
068    private final SaveLayersModel model = new SaveLayersModel();
069    private UserAction action = UserAction.CANCEL;
070    private final UploadAndSaveProgressRenderer pnlUploadLayers = new UploadAndSaveProgressRenderer();
071
072    private final SaveAndProceedAction saveAndProceedAction = new SaveAndProceedAction();
073    private final SaveSessionAction saveSessionAction = new SaveSessionAction();
074    private final DiscardAndProceedAction discardAndProceedAction = new DiscardAndProceedAction();
075    private final CancelAction cancelAction = new CancelAction();
076    private transient SaveAndUploadTask saveAndUploadTask;
077
078    private final JButton saveAndProceedActionButton = new JButton(saveAndProceedAction);
079
080    /**
081     * Constructs a new {@code SaveLayersDialog}.
082     * @param parent parent component
083     */
084    public SaveLayersDialog(Component parent) {
085        super(GuiHelper.getFrameForComponent(parent), ModalityType.DOCUMENT_MODAL);
086        build();
087    }
088
089    /**
090     * builds the GUI
091     */
092    protected void build() {
093        WindowGeometry geometry = WindowGeometry.centerOnScreen(new Dimension(650, 300));
094        geometry.applySafe(this);
095        getContentPane().setLayout(new BorderLayout());
096
097        SaveLayersTable table = new SaveLayersTable(model);
098        JScrollPane pane = new JScrollPane(table);
099        model.addPropertyChangeListener(table);
100        table.getModel().addTableModelListener(this);
101
102        getContentPane().add(pane, BorderLayout.CENTER);
103        getContentPane().add(buildButtonRow(), BorderLayout.SOUTH);
104
105        addWindowListener(new WindowClosingAdapter());
106        setDefaultCloseOperation(WindowConstants.DISPOSE_ON_CLOSE);
107    }
108
109    /**
110     * builds the button row
111     *
112     * @return the panel with the button row
113     */
114    protected JPanel buildButtonRow() {
115        JPanel pnl = new JPanel(new GridBagLayout());
116
117        model.addPropertyChangeListener(saveAndProceedAction);
118        pnl.add(saveAndProceedActionButton, GBC.std(0, 0).insets(5, 5, 0, 0).fill(GBC.HORIZONTAL));
119
120        pnl.add(new JButton(saveSessionAction), GBC.std(1, 0).insets(5, 5, 5, 0).fill(GBC.HORIZONTAL));
121
122        model.addPropertyChangeListener(discardAndProceedAction);
123        pnl.add(new JButton(discardAndProceedAction), GBC.std(0, 1).insets(5, 5, 0, 5).fill(GBC.HORIZONTAL));
124
125        pnl.add(new JButton(cancelAction), GBC.std(1, 1).insets(5, 5, 5, 5).fill(GBC.HORIZONTAL));
126
127        JPanel pnl2 = new JPanel(new BorderLayout());
128        pnl2.add(pnlUploadLayers, BorderLayout.CENTER);
129        model.addPropertyChangeListener(pnlUploadLayers);
130        pnl2.add(pnl, BorderLayout.SOUTH);
131        return pnl2;
132    }
133
134    public void prepareForSavingAndUpdatingLayersBeforeExit() {
135        setTitle(tr("Unsaved changes - Save/Upload before exiting?"));
136        this.saveAndProceedAction.initForSaveAndExit();
137        this.discardAndProceedAction.initForDiscardAndExit();
138    }
139
140    public void prepareForSavingAndUpdatingLayersBeforeDelete() {
141        setTitle(tr("Unsaved changes - Save/Upload before deleting?"));
142        this.saveAndProceedAction.initForSaveAndDelete();
143        this.discardAndProceedAction.initForDiscardAndDelete();
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();
314                break;
315            case UPLOADING_AND_SAVING: cancelSafeAndUploadTask();
316                break;
317            }
318        }
319
320        @Override
321        public void actionPerformed(ActionEvent e) {
322            cancel();
323        }
324    }
325
326    class DiscardAndProceedAction extends AbstractAction  implements PropertyChangeListener {
327        DiscardAndProceedAction() {
328            initForDiscardAndExit();
329        }
330
331        public void initForDiscardAndExit() {
332            putValue(NAME, tr("Exit now!"));
333            putValue(SHORT_DESCRIPTION, tr("Exit JOSM without saving. Unsaved changes are lost."));
334            putValue(SMALL_ICON, ImageProvider.get("exit"));
335        }
336
337        public void initForDiscardAndDelete() {
338            putValue(NAME, tr("Delete now!"));
339            putValue(SHORT_DESCRIPTION, tr("Delete layers without saving. Unsaved changes are lost."));
340            putValue(SMALL_ICON, ImageProvider.get("dialogs", "delete"));
341        }
342
343        @Override
344        public void actionPerformed(ActionEvent e) {
345            setUserAction(UserAction.PROCEED);
346            closeDialog();
347        }
348
349        @Override
350        public void propertyChange(PropertyChangeEvent evt) {
351            if (evt.getPropertyName().equals(SaveLayersModel.MODE_PROP)) {
352                Mode mode = (Mode) evt.getNewValue();
353                switch(mode) {
354                case EDITING_DATA: setEnabled(true);
355                    break;
356                case UPLOADING_AND_SAVING: setEnabled(false);
357                    break;
358                }
359            }
360        }
361    }
362
363    class SaveSessionAction extends SessionSaveAsAction {
364
365        SaveSessionAction() {
366            super(false, false);
367        }
368
369        @Override
370        public void actionPerformed(ActionEvent e) {
371            try {
372                saveSession();
373                setUserAction(UserAction.PROCEED);
374                closeDialog();
375            } catch (UserCancelException ignore) {
376                if (Main.isTraceEnabled()) {
377                    Main.trace(ignore.getMessage());
378                }
379            }
380        }
381    }
382
383    final class SaveAndProceedAction extends AbstractAction implements PropertyChangeListener {
384        private static final int ICON_SIZE = 24;
385        private static final String BASE_ICON = "BASE_ICON";
386        private final transient Image save = ImageProvider.get("save").getImage();
387        private final transient Image upld = ImageProvider.get("upload").getImage();
388        private final transient Image saveDis = new BufferedImage(ICON_SIZE, ICON_SIZE, BufferedImage.TYPE_4BYTE_ABGR);
389        private final transient Image upldDis = new BufferedImage(ICON_SIZE, ICON_SIZE, BufferedImage.TYPE_4BYTE_ABGR);
390
391        SaveAndProceedAction() {
392            // get disabled versions of icons
393            new JLabel(ImageProvider.get("save")).getDisabledIcon().paintIcon(new JPanel(), saveDis.getGraphics(), 0, 0);
394            new JLabel(ImageProvider.get("upload")).getDisabledIcon().paintIcon(new JPanel(), upldDis.getGraphics(), 0, 0);
395            initForSaveAndExit();
396        }
397
398        public void initForSaveAndExit() {
399            putValue(NAME, tr("Perform actions before exiting"));
400            putValue(SHORT_DESCRIPTION, tr("Exit JOSM with saving. Unsaved changes are uploaded and/or saved."));
401            putValue(BASE_ICON, ImageProvider.get("exit"));
402            redrawIcon();
403        }
404
405        public void initForSaveAndDelete() {
406            putValue(NAME, tr("Perform actions before deleting"));
407            putValue(SHORT_DESCRIPTION, tr("Save/Upload layers before deleting. Unsaved changes are not lost."));
408            putValue(BASE_ICON, ImageProvider.get("dialogs", "delete"));
409            redrawIcon();
410        }
411
412        public void redrawIcon() {
413            Image base = ((ImageIcon) getValue(BASE_ICON)).getImage();
414            BufferedImage newIco = new BufferedImage(ICON_SIZE*3, ICON_SIZE, BufferedImage.TYPE_4BYTE_ABGR);
415            Graphics2D g = newIco.createGraphics();
416            g.drawImage(model.getLayersToUpload().isEmpty() ? upldDis : upld, ICON_SIZE*0, 0, ICON_SIZE, ICON_SIZE, null);
417            g.drawImage(model.getLayersToSave().isEmpty()   ? saveDis : save, ICON_SIZE*1, 0, ICON_SIZE, ICON_SIZE, null);
418            g.drawImage(base,                                                 ICON_SIZE*2, 0, ICON_SIZE, ICON_SIZE, null);
419            putValue(SMALL_ICON, new ImageIcon(newIco));
420        }
421
422        @Override
423        public void actionPerformed(ActionEvent e) {
424            if (!confirmSaveLayerInfosOK(model))
425                return;
426            launchSafeAndUploadTask();
427        }
428
429        @Override
430        public void propertyChange(PropertyChangeEvent evt) {
431            if (evt.getPropertyName().equals(SaveLayersModel.MODE_PROP)) {
432                SaveLayersModel.Mode mode = (SaveLayersModel.Mode) evt.getNewValue();
433                switch(mode) {
434                case EDITING_DATA: setEnabled(true);
435                    break;
436                case UPLOADING_AND_SAVING: setEnabled(false);
437                    break;
438                }
439            }
440        }
441    }
442
443    /**
444     * This is the asynchronous task which uploads modified layers to the server and
445     * saves them to files, if requested by the user.
446     *
447     */
448    protected class SaveAndUploadTask implements Runnable {
449
450        private final SaveLayersModel model;
451        private final ProgressMonitor monitor;
452        private final ExecutorService worker;
453        private boolean canceled;
454        private Future<?> currentFuture;
455        private AbstractIOTask currentTask;
456
457        public SaveAndUploadTask(SaveLayersModel model, ProgressMonitor monitor) {
458            this.model = model;
459            this.monitor = monitor;
460            this.worker = Executors.newSingleThreadExecutor(Utils.newThreadFactory(getClass() + "-%d", Thread.NORM_PRIORITY));
461        }
462
463        protected void uploadLayers(List<SaveLayerInfo> toUpload) {
464            for (final SaveLayerInfo layerInfo: toUpload) {
465                AbstractModifiableLayer layer = layerInfo.getLayer();
466                if (canceled) {
467                    model.setUploadState(layer, UploadOrSaveState.CANCELED);
468                    continue;
469                }
470                monitor.subTask(tr("Preparing layer ''{0}'' for upload ...", layerInfo.getName()));
471
472                if (!UploadAction.checkPreUploadConditions(layer)) {
473                    model.setUploadState(layer, UploadOrSaveState.FAILED);
474                    continue;
475                }
476
477                AbstractUploadDialog dialog = layer.getUploadDialog();
478                if (dialog != null) {
479                    dialog.setVisible(true);
480                    if (dialog.isCanceled()) {
481                        model.setUploadState(layer, UploadOrSaveState.CANCELED);
482                        continue;
483                    }
484                    dialog.rememberUserInput();
485                }
486
487                currentTask = layer.createUploadTask(monitor);
488                if (currentTask == null) {
489                    model.setUploadState(layer, UploadOrSaveState.FAILED);
490                    continue;
491                }
492                currentFuture = worker.submit(currentTask);
493                try {
494                    // wait for the asynchronous task to complete
495                    //
496                    currentFuture.get();
497                } catch (CancellationException e) {
498                    model.setUploadState(layer, UploadOrSaveState.CANCELED);
499                } catch (InterruptedException | ExecutionException e) {
500                    Main.error(e);
501                    model.setUploadState(layer, UploadOrSaveState.FAILED);
502                    ExceptionDialogUtil.explainException(e);
503                }
504                if (currentTask.isCanceled()) {
505                    model.setUploadState(layer, UploadOrSaveState.CANCELED);
506                } else if (currentTask.isFailed()) {
507                    Main.error(currentTask.getLastException());
508                    ExceptionDialogUtil.explainException(currentTask.getLastException());
509                    model.setUploadState(layer, UploadOrSaveState.FAILED);
510                } else {
511                    model.setUploadState(layer, UploadOrSaveState.OK);
512                }
513                currentTask = null;
514                currentFuture = null;
515            }
516        }
517
518        protected void saveLayers(List<SaveLayerInfo> toSave) {
519            for (final SaveLayerInfo layerInfo: toSave) {
520                if (canceled) {
521                    model.setSaveState(layerInfo.getLayer(), UploadOrSaveState.CANCELED);
522                    continue;
523                }
524                // Check save preconditions earlier to avoid a blocking reentring call to EDT (see #10086)
525                if (layerInfo.isDoCheckSaveConditions()) {
526                    if (!layerInfo.getLayer().checkSaveConditions()) {
527                        continue;
528                    }
529                    layerInfo.setDoCheckSaveConditions(false);
530                }
531                currentTask = new SaveLayerTask(layerInfo, monitor);
532                currentFuture = worker.submit(currentTask);
533
534                try {
535                    // wait for the asynchronous task to complete
536                    //
537                    currentFuture.get();
538                } catch (CancellationException e) {
539                    model.setSaveState(layerInfo.getLayer(), UploadOrSaveState.CANCELED);
540                } catch (InterruptedException | ExecutionException e) {
541                    Main.error(e);
542                    model.setSaveState(layerInfo.getLayer(), UploadOrSaveState.FAILED);
543                    ExceptionDialogUtil.explainException(e);
544                }
545                if (currentTask.isCanceled()) {
546                    model.setSaveState(layerInfo.getLayer(), UploadOrSaveState.CANCELED);
547                } else if (currentTask.isFailed()) {
548                    if (currentTask.getLastException() != null) {
549                        Main.error(currentTask.getLastException());
550                        ExceptionDialogUtil.explainException(currentTask.getLastException());
551                    }
552                    model.setSaveState(layerInfo.getLayer(), UploadOrSaveState.FAILED);
553                } else {
554                    model.setSaveState(layerInfo.getLayer(), UploadOrSaveState.OK);
555                }
556                this.currentTask = null;
557                this.currentFuture = null;
558            }
559        }
560
561        protected void warnBecauseOfUnsavedData() {
562            int numProblems = model.getNumCancel() + model.getNumFailed();
563            if (numProblems == 0)
564                return;
565            Main.warn(numProblems + " problems occured during upload/save");
566            String msg = trn(
567                    "<html>An upload and/or save operation of one layer with modifications<br>"
568                    + "was canceled or has failed.</html>",
569                    "<html>Upload and/or save operations of {0} layers with modifications<br>"
570                    + "were canceled or have failed.</html>",
571                    numProblems,
572                    numProblems
573            );
574            JOptionPane.showMessageDialog(
575                    Main.parent,
576                    msg,
577                    tr("Incomplete upload and/or save"),
578                    JOptionPane.WARNING_MESSAGE
579            );
580        }
581
582        @Override
583        public void run() {
584            GuiHelper.runInEDTAndWait(new Runnable() {
585                @Override
586                public void run() {
587                    model.setMode(SaveLayersModel.Mode.UPLOADING_AND_SAVING);
588                    List<SaveLayerInfo> toUpload = model.getLayersToUpload();
589                    if (!toUpload.isEmpty()) {
590                        uploadLayers(toUpload);
591                    }
592                    List<SaveLayerInfo> toSave = model.getLayersToSave();
593                    if (!toSave.isEmpty()) {
594                        saveLayers(toSave);
595                    }
596                    model.setMode(SaveLayersModel.Mode.EDITING_DATA);
597                    if (model.hasUnsavedData()) {
598                        warnBecauseOfUnsavedData();
599                        model.setMode(Mode.EDITING_DATA);
600                        if (canceled) {
601                            setUserAction(UserAction.CANCEL);
602                            closeDialog();
603                        }
604                    } else {
605                        setUserAction(UserAction.PROCEED);
606                        closeDialog();
607                    }
608                }
609            });
610            worker.shutdownNow();
611        }
612
613        public void cancel() {
614            if (currentTask != null) {
615                currentTask.cancel();
616            }
617            worker.shutdown();
618            canceled = true;
619        }
620    }
621
622    @Override
623    public void tableChanged(TableModelEvent arg0) {
624        boolean dis = model.getLayersToSave().isEmpty() && model.getLayersToUpload().isEmpty();
625        if (saveAndProceedActionButton != null) {
626            saveAndProceedActionButton.setEnabled(!dis);
627        }
628        saveAndProceedAction.redrawIcon();
629    }
630}