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