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