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