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