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        static final class LayerCellRenderer implements ListCellRenderer<SaveLayerInfo> {
228            private final DefaultListCellRenderer def = new DefaultListCellRenderer();
229
230            @Override
231            public Component getListCellRendererComponent(JList<? extends SaveLayerInfo> list, SaveLayerInfo info, int index,
232                    boolean isSelected, boolean cellHasFocus) {
233                def.setIcon(info.getLayer().getIcon());
234                def.setText(info.getName());
235                return def;
236            }
237        }
238
239        private final JLabel lblMessage = new JLabel();
240        private final JList<SaveLayerInfo> lstLayers = new JList<>();
241
242        LayerListWarningMessagePanel(String msg, List<SaveLayerInfo> infos) {
243            super(new GridBagLayout());
244            build();
245            lblMessage.setText(msg);
246            lstLayers.setListData(infos.toArray(new SaveLayerInfo[infos.size()]));
247        }
248
249        protected void build() {
250            GridBagConstraints gc = new GridBagConstraints();
251            gc.gridx = 0;
252            gc.gridy = 0;
253            gc.fill = GridBagConstraints.HORIZONTAL;
254            gc.weightx = 1.0;
255            gc.weighty = 0.0;
256            add(lblMessage, gc);
257            lblMessage.setHorizontalAlignment(JLabel.LEFT);
258            lstLayers.setCellRenderer(new LayerCellRenderer());
259            gc.gridx = 0;
260            gc.gridy = 1;
261            gc.fill = GridBagConstraints.HORIZONTAL;
262            gc.weightx = 1.0;
263            gc.weighty = 1.0;
264            add(lstLayers, gc);
265        }
266    }
267
268    private static void warn(String msg, List<SaveLayerInfo> infos, String title) {
269        JPanel panel = new LayerListWarningMessagePanel(msg, infos);
270        // For unit test coverage in headless mode
271        if (!GraphicsEnvironment.isHeadless()) {
272            JOptionPane.showConfirmDialog(Main.parent, panel, title, JOptionPane.DEFAULT_OPTION, JOptionPane.WARNING_MESSAGE);
273        }
274    }
275
276    protected static void warnLayersWithConflictsAndUploadRequest(List<SaveLayerInfo> infos) {
277        warn(trn("<html>{0} layer has unresolved conflicts.<br>"
278                + "Either resolve them first or discard the modifications.<br>"
279                + "Layer with conflicts:</html>",
280                "<html>{0} layers have unresolved conflicts.<br>"
281                + "Either resolve them first or discard the modifications.<br>"
282                + "Layers with conflicts:</html>",
283                infos.size(),
284                infos.size()),
285             infos, tr("Unsaved data and conflicts"));
286    }
287
288    protected static void warnLayersWithoutFilesAndSaveRequest(List<SaveLayerInfo> infos) {
289        warn(trn("<html>{0} layer needs saving but has no associated file.<br>"
290                + "Either select a file for this layer or discard the changes.<br>"
291                + "Layer without a file:</html>",
292                "<html>{0} layers need saving but have no associated file.<br>"
293                + "Either select a file for each of them or discard the changes.<br>"
294                + "Layers without a file:</html>",
295                infos.size(),
296                infos.size()),
297             infos, tr("Unsaved data and missing associated file"));
298    }
299
300    protected static void warnLayersWithIllegalFilesAndSaveRequest(List<SaveLayerInfo> infos) {
301        warn(trn("<html>{0} layer needs saving but has an associated file<br>"
302                + "which cannot be written.<br>"
303                + "Either select another file for this layer or discard the changes.<br>"
304                + "Layer with a non-writable file:</html>",
305                "<html>{0} layers need saving but have associated files<br>"
306                + "which cannot be written.<br>"
307                + "Either select another file for each of them or discard the changes.<br>"
308                + "Layers with non-writable files:</html>",
309                infos.size(),
310                infos.size()),
311             infos, tr("Unsaved data non-writable files"));
312    }
313
314    static boolean confirmSaveLayerInfosOK(SaveLayersModel model) {
315        List<SaveLayerInfo> layerInfos = model.getLayersWithConflictsAndUploadRequest();
316        if (!layerInfos.isEmpty()) {
317            warnLayersWithConflictsAndUploadRequest(layerInfos);
318            return false;
319        }
320
321        layerInfos = model.getLayersWithoutFilesAndSaveRequest();
322        if (!layerInfos.isEmpty()) {
323            warnLayersWithoutFilesAndSaveRequest(layerInfos);
324            return false;
325        }
326
327        layerInfos = model.getLayersWithIllegalFilesAndSaveRequest();
328        if (!layerInfos.isEmpty()) {
329            warnLayersWithIllegalFilesAndSaveRequest(layerInfos);
330            return false;
331        }
332
333        return true;
334    }
335
336    protected void setUserAction(UserAction action) {
337        this.action = action;
338    }
339
340    /**
341     * Closes this dialog and frees all native screen resources.
342     */
343    public void closeDialog() {
344        setVisible(false);
345        dispose();
346    }
347
348    class WindowClosingAdapter extends WindowAdapter {
349        @Override
350        public void windowClosing(WindowEvent e) {
351            cancelAction.cancel();
352        }
353    }
354
355    class CancelAction extends AbstractAction {
356        CancelAction() {
357            putValue(NAME, tr("Cancel"));
358            putValue(SHORT_DESCRIPTION, tr("Close this dialog and resume editing in JOSM"));
359            putValue(SMALL_ICON, ImageProvider.get("cancel"));
360            InputMapUtils.addEscapeAction(getRootPane(), this);
361        }
362
363        protected void cancelWhenInEditingModel() {
364            setUserAction(UserAction.CANCEL);
365            closeDialog();
366        }
367
368        public void cancel() {
369            switch(model.getMode()) {
370            case EDITING_DATA: cancelWhenInEditingModel();
371                break;
372            case UPLOADING_AND_SAVING: cancelSafeAndUploadTask();
373                break;
374            }
375        }
376
377        @Override
378        public void actionPerformed(ActionEvent e) {
379            cancel();
380        }
381    }
382
383    class DiscardAndProceedAction extends AbstractAction implements PropertyChangeListener {
384        DiscardAndProceedAction() {
385            initForReason(Reason.EXIT);
386        }
387
388        public void initForReason(Reason reason) {
389            switch (reason) {
390                case EXIT:
391                    putValue(NAME, tr("Exit now!"));
392                    putValue(SHORT_DESCRIPTION, tr("Exit JOSM without saving. Unsaved changes are lost."));
393                    putValue(SMALL_ICON, ImageProvider.get("exit"));
394                    break;
395                case RESTART:
396                    putValue(NAME, tr("Restart now!"));
397                    putValue(SHORT_DESCRIPTION, tr("Restart JOSM without saving. Unsaved changes are lost."));
398                    putValue(SMALL_ICON, ImageProvider.get("restart"));
399                    break;
400                case DELETE:
401                    putValue(NAME, tr("Delete now!"));
402                    putValue(SHORT_DESCRIPTION, tr("Delete layers without saving. Unsaved changes are lost."));
403                    putValue(SMALL_ICON, ImageProvider.get("dialogs", "delete"));
404                    break;
405            }
406
407        }
408
409        @Override
410        public void actionPerformed(ActionEvent e) {
411            setUserAction(UserAction.PROCEED);
412            closeDialog();
413        }
414
415        @Override
416        public void propertyChange(PropertyChangeEvent evt) {
417            if (evt.getPropertyName().equals(SaveLayersModel.MODE_PROP)) {
418                Mode mode = (Mode) evt.getNewValue();
419                switch(mode) {
420                case EDITING_DATA: setEnabled(true);
421                    break;
422                case UPLOADING_AND_SAVING: setEnabled(false);
423                    break;
424                }
425            }
426        }
427    }
428
429    class SaveSessionAction extends SessionSaveAsAction {
430
431        SaveSessionAction() {
432            super(false, false);
433        }
434
435        @Override
436        public void actionPerformed(ActionEvent e) {
437            try {
438                saveSession();
439                setUserAction(UserAction.PROCEED);
440                closeDialog();
441            } catch (UserCancelException ignore) {
442                Main.trace(ignore);
443            }
444        }
445    }
446
447    final class SaveAndProceedAction extends AbstractAction implements PropertyChangeListener {
448        private static final int ICON_SIZE = 24;
449        private static final String BASE_ICON = "BASE_ICON";
450        private final transient Image save = getImage("save", false);
451        private final transient Image upld = getImage("upload", false);
452        private final transient Image saveDis = getImage("save", true);
453        private final transient Image upldDis = getImage("upload", true);
454
455        SaveAndProceedAction() {
456            initForReason(Reason.EXIT);
457        }
458
459        Image getImage(String name, boolean disabled) {
460            ImageIcon img = new ImageProvider(name).setDisabled(disabled).get();
461            return img != null ? img.getImage() : null;
462        }
463
464        public void initForReason(Reason reason) {
465            switch (reason) {
466                case EXIT:
467                    putValue(NAME, tr("Perform actions before exiting"));
468                    putValue(SHORT_DESCRIPTION, tr("Exit JOSM with saving. Unsaved changes are uploaded and/or saved."));
469                    putValue(BASE_ICON, ImageProvider.get("exit"));
470                    break;
471                case RESTART:
472                    putValue(NAME, tr("Perform actions before restarting"));
473                    putValue(SHORT_DESCRIPTION, tr("Restart JOSM with saving. Unsaved changes are uploaded and/or saved."));
474                    putValue(BASE_ICON, ImageProvider.get("restart"));
475                    break;
476                case DELETE:
477                    putValue(NAME, tr("Perform actions before deleting"));
478                    putValue(SHORT_DESCRIPTION, tr("Save/Upload layers before deleting. Unsaved changes are not lost."));
479                    putValue(BASE_ICON, ImageProvider.get("dialogs", "delete"));
480                    break;
481            }
482            redrawIcon();
483        }
484
485        public void redrawIcon() {
486            Image base = ((ImageIcon) getValue(BASE_ICON)).getImage();
487            BufferedImage newIco = new BufferedImage(ICON_SIZE*3, ICON_SIZE, BufferedImage.TYPE_4BYTE_ABGR);
488            Graphics2D g = newIco.createGraphics();
489            // CHECKSTYLE.OFF: SingleSpaceSeparator
490            g.drawImage(model.getLayersToUpload().isEmpty() ? upldDis : upld, ICON_SIZE*0, 0, ICON_SIZE, ICON_SIZE, null);
491            g.drawImage(model.getLayersToSave().isEmpty()   ? saveDis : save, ICON_SIZE*1, 0, ICON_SIZE, ICON_SIZE, null);
492            g.drawImage(base,                                                 ICON_SIZE*2, 0, ICON_SIZE, ICON_SIZE, null);
493            // CHECKSTYLE.ON: SingleSpaceSeparator
494            putValue(SMALL_ICON, new ImageIcon(newIco));
495        }
496
497        @Override
498        public void actionPerformed(ActionEvent e) {
499            if (!confirmSaveLayerInfosOK(model))
500                return;
501            launchSafeAndUploadTask();
502        }
503
504        @Override
505        public void propertyChange(PropertyChangeEvent evt) {
506            if (evt.getPropertyName().equals(SaveLayersModel.MODE_PROP)) {
507                SaveLayersModel.Mode mode = (SaveLayersModel.Mode) evt.getNewValue();
508                switch(mode) {
509                case EDITING_DATA: setEnabled(true);
510                    break;
511                case UPLOADING_AND_SAVING: setEnabled(false);
512                    break;
513                }
514            }
515        }
516    }
517
518    /**
519     * This is the asynchronous task which uploads modified layers to the server and
520     * saves them to files, if requested by the user.
521     *
522     */
523    protected class SaveAndUploadTask implements Runnable {
524
525        private final SaveLayersModel model;
526        private final ProgressMonitor monitor;
527        private final ExecutorService worker;
528        private boolean canceled;
529        private Future<?> currentFuture;
530        private AbstractIOTask currentTask;
531
532        public SaveAndUploadTask(SaveLayersModel model, ProgressMonitor monitor) {
533            this.model = model;
534            this.monitor = monitor;
535            this.worker = Executors.newSingleThreadExecutor(Utils.newThreadFactory(getClass() + "-%d", Thread.NORM_PRIORITY));
536        }
537
538        protected void uploadLayers(List<SaveLayerInfo> toUpload) {
539            for (final SaveLayerInfo layerInfo: toUpload) {
540                AbstractModifiableLayer layer = layerInfo.getLayer();
541                if (canceled) {
542                    model.setUploadState(layer, UploadOrSaveState.CANCELED);
543                    continue;
544                }
545                monitor.subTask(tr("Preparing layer ''{0}'' for upload ...", layerInfo.getName()));
546
547                if (!UploadAction.checkPreUploadConditions(layer)) {
548                    model.setUploadState(layer, UploadOrSaveState.FAILED);
549                    continue;
550                }
551
552                AbstractUploadDialog dialog = layer.getUploadDialog();
553                if (dialog != null) {
554                    dialog.setVisible(true);
555                    if (dialog.isCanceled()) {
556                        model.setUploadState(layer, UploadOrSaveState.CANCELED);
557                        continue;
558                    }
559                    dialog.rememberUserInput();
560                }
561
562                currentTask = layer.createUploadTask(monitor);
563                if (currentTask == null) {
564                    model.setUploadState(layer, UploadOrSaveState.FAILED);
565                    continue;
566                }
567                currentFuture = worker.submit(currentTask);
568                try {
569                    // wait for the asynchronous task to complete
570                    //
571                    currentFuture.get();
572                } catch (CancellationException e) {
573                    Main.trace(e);
574                    model.setUploadState(layer, UploadOrSaveState.CANCELED);
575                } catch (InterruptedException | ExecutionException e) {
576                    Main.error(e);
577                    model.setUploadState(layer, UploadOrSaveState.FAILED);
578                    ExceptionDialogUtil.explainException(e);
579                }
580                if (currentTask.isCanceled()) {
581                    model.setUploadState(layer, UploadOrSaveState.CANCELED);
582                } else if (currentTask.isFailed()) {
583                    Main.error(currentTask.getLastException());
584                    ExceptionDialogUtil.explainException(currentTask.getLastException());
585                    model.setUploadState(layer, UploadOrSaveState.FAILED);
586                } else {
587                    model.setUploadState(layer, UploadOrSaveState.OK);
588                }
589                currentTask = null;
590                currentFuture = null;
591            }
592        }
593
594        protected void saveLayers(List<SaveLayerInfo> toSave) {
595            for (final SaveLayerInfo layerInfo: toSave) {
596                if (canceled) {
597                    model.setSaveState(layerInfo.getLayer(), UploadOrSaveState.CANCELED);
598                    continue;
599                }
600                // Check save preconditions earlier to avoid a blocking reentring call to EDT (see #10086)
601                if (layerInfo.isDoCheckSaveConditions()) {
602                    if (!layerInfo.getLayer().checkSaveConditions()) {
603                        continue;
604                    }
605                    layerInfo.setDoCheckSaveConditions(false);
606                }
607                currentTask = new SaveLayerTask(layerInfo, monitor);
608                currentFuture = worker.submit(currentTask);
609
610                try {
611                    // wait for the asynchronous task to complete
612                    //
613                    currentFuture.get();
614                } catch (CancellationException e) {
615                    Main.trace(e);
616                    model.setSaveState(layerInfo.getLayer(), UploadOrSaveState.CANCELED);
617                } catch (InterruptedException | ExecutionException e) {
618                    Main.error(e);
619                    model.setSaveState(layerInfo.getLayer(), UploadOrSaveState.FAILED);
620                    ExceptionDialogUtil.explainException(e);
621                }
622                if (currentTask.isCanceled()) {
623                    model.setSaveState(layerInfo.getLayer(), UploadOrSaveState.CANCELED);
624                } else if (currentTask.isFailed()) {
625                    if (currentTask.getLastException() != null) {
626                        Main.error(currentTask.getLastException());
627                        ExceptionDialogUtil.explainException(currentTask.getLastException());
628                    }
629                    model.setSaveState(layerInfo.getLayer(), UploadOrSaveState.FAILED);
630                } else {
631                    model.setSaveState(layerInfo.getLayer(), UploadOrSaveState.OK);
632                }
633                this.currentTask = null;
634                this.currentFuture = null;
635            }
636        }
637
638        protected void warnBecauseOfUnsavedData() {
639            int numProblems = model.getNumCancel() + model.getNumFailed();
640            if (numProblems == 0)
641                return;
642            Main.warn(numProblems + " problems occured during upload/save");
643            String msg = trn(
644                    "<html>An upload and/or save operation of one layer with modifications<br>"
645                    + "was canceled or has failed.</html>",
646                    "<html>Upload and/or save operations of {0} layers with modifications<br>"
647                    + "were canceled or have failed.</html>",
648                    numProblems,
649                    numProblems
650            );
651            JOptionPane.showMessageDialog(
652                    Main.parent,
653                    msg,
654                    tr("Incomplete upload and/or save"),
655                    JOptionPane.WARNING_MESSAGE
656            );
657        }
658
659        @Override
660        public void run() {
661            GuiHelper.runInEDTAndWait(() -> {
662                model.setMode(SaveLayersModel.Mode.UPLOADING_AND_SAVING);
663                List<SaveLayerInfo> toUpload = model.getLayersToUpload();
664                if (!toUpload.isEmpty()) {
665                    uploadLayers(toUpload);
666                }
667                List<SaveLayerInfo> toSave = model.getLayersToSave();
668                if (!toSave.isEmpty()) {
669                    saveLayers(toSave);
670                }
671                model.setMode(SaveLayersModel.Mode.EDITING_DATA);
672                if (model.hasUnsavedData()) {
673                    warnBecauseOfUnsavedData();
674                    model.setMode(Mode.EDITING_DATA);
675                    if (canceled) {
676                        setUserAction(UserAction.CANCEL);
677                        closeDialog();
678                    }
679                } else {
680                    setUserAction(UserAction.PROCEED);
681                    closeDialog();
682                }
683            });
684            worker.shutdownNow();
685        }
686
687        public void cancel() {
688            if (currentTask != null) {
689                currentTask.cancel();
690            }
691            worker.shutdown();
692            canceled = true;
693        }
694    }
695
696    @Override
697    public void tableChanged(TableModelEvent arg0) {
698        boolean dis = model.getLayersToSave().isEmpty() && model.getLayersToUpload().isEmpty();
699        if (saveAndProceedActionButton != null) {
700            saveAndProceedActionButton.setEnabled(!dis);
701        }
702        saveAndProceedAction.redrawIcon();
703    }
704}