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}