Index: trunk/src/org/openstreetmap/josm/gui/ExtendedDialog.java
===================================================================
--- trunk/src/org/openstreetmap/josm/gui/ExtendedDialog.java	(revision 2021)
+++ trunk/src/org/openstreetmap/josm/gui/ExtendedDialog.java	(revision 2025)
@@ -3,4 +3,5 @@
 import java.awt.Component;
 import java.awt.Dimension;
+import java.awt.Frame;
 import java.awt.GridBagLayout;
 import java.awt.Toolkit;
@@ -202,5 +203,5 @@
         super.setVisible(visible);
         if (visible) {
-            toFront();
+            repaint();
         }
     }
Index: trunk/src/org/openstreetmap/josm/gui/MainApplication.java
===================================================================
--- trunk/src/org/openstreetmap/josm/gui/MainApplication.java	(revision 2021)
+++ trunk/src/org/openstreetmap/josm/gui/MainApplication.java	(revision 2025)
@@ -47,5 +47,5 @@
         mainFrame.addWindowListener(new WindowAdapter(){
             @Override public void windowClosing(final WindowEvent arg0) {
-                if (Main.breakBecauseUnsavedChanges())
+                if (!Main.saveUnsavedModifications())
                     return;
                 Main.saveGuiGeometry();
@@ -73,12 +73,14 @@
         final Map<String, Collection<String>> args = new HashMap<String, Collection<String>>();
         for (String arg : argArray) {
-            if (!arg.startsWith("--"))
+            if (!arg.startsWith("--")) {
                 arg = "--download="+arg;
+            }
             int i = arg.indexOf('=');
             String key = i == -1 ? arg.substring(2) : arg.substring(2,i);
             String value = i == -1 ? "" : arg.substring(i+1);
             Collection<String> v = args.get(key);
-            if (v == null)
+            if (v == null) {
                 v = new LinkedList<String>();
+            }
             v.add(value);
             args.put(key, v);
@@ -88,8 +90,9 @@
 
         // Check if passed as parameter
-        if (args.containsKey("language"))
+        if (args.containsKey("language")) {
             I18n.set((String)(args.get("language").toArray()[0]));
-        else
+        } else {
             I18n.set(Main.pref.get("language", null));
+        }
 
         if (argList.contains("--help") || argList.contains("-?") || argList.contains("-h")) {
@@ -143,7 +146,8 @@
 
         if (((!args.containsKey("no-maximize") && !args.containsKey("geometry")
-        && Main.pref.get("gui.geometry").length() == 0) || args.containsKey("maximize"))
-        && Toolkit.getDefaultToolkit().isFrameStateSupported(JFrame.MAXIMIZED_BOTH))
+                && Main.pref.get("gui.geometry").length() == 0) || args.containsKey("maximize"))
+                && Toolkit.getDefaultToolkit().isFrameStateSupported(JFrame.MAXIMIZED_BOTH)) {
             mainFrame.setExtendedState(JFrame.MAXIMIZED_BOTH);
+        }
 
         EventQueue.invokeLater(new Runnable() {
@@ -161,11 +165,13 @@
     public static void removeObsoletePreferences() {
         String[] obsolete = {
-           "sample.preference.that.does.not.exist", // sample comment, expiry date should go here
-           "osm-server.version", // remove this around 10/2009
-           "osm-server.additional-versions", // remove this around 10/2009
-           null
+                "sample.preference.that.does.not.exist", // sample comment, expiry date should go here
+                "osm-server.version", // remove this around 10/2009
+                "osm-server.additional-versions", // remove this around 10/2009
+                null
         };
         for (String i : obsolete) {
-            if (i == null) continue;
+            if (i == null) {
+                continue;
+            }
             if (Main.pref.hasKey(i)) {
                 Main.pref.removeFromCollection(i, Main.pref.get(i));
Index: trunk/src/org/openstreetmap/josm/gui/MapStatus.java
===================================================================
--- trunk/src/org/openstreetmap/josm/gui/MapStatus.java	(revision 2021)
+++ trunk/src/org/openstreetmap/josm/gui/MapStatus.java	(revision 2025)
@@ -194,10 +194,10 @@
                             final StringBuilder text = new StringBuilder();
                             String name = osm.getDisplayName(DefaultNameFormatter.getInstance());
-                            if (osm.id == 0 || osm.modified) {
+                            if (osm.getId() == 0 || osm.isModified()) {
                                 name = "<i><b>"+ osm.getDisplayName(DefaultNameFormatter.getInstance())+"*</b></i>";
                             }
                             text.append(name);
-                            if (osm.id != 0) {
-                                text.append("<br>id="+osm.id);
+                            if (osm.getId() != 0) {
+                                text.append("<br>id="+osm.getId());
                             }
                             for (Entry<String, String> e : osm.entrySet()) {
Index: trunk/src/org/openstreetmap/josm/gui/MapView.java
===================================================================
--- trunk/src/org/openstreetmap/josm/gui/MapView.java	(revision 2021)
+++ trunk/src/org/openstreetmap/josm/gui/MapView.java	(revision 2025)
@@ -44,5 +44,4 @@
 import org.openstreetmap.josm.gui.layer.MapViewPaintable;
 import org.openstreetmap.josm.gui.layer.OsmDataLayer;
-import org.openstreetmap.josm.gui.layer.OsmDataLayer.ModifiedChangedListener;
 import org.openstreetmap.josm.gui.layer.markerlayer.MarkerLayer;
 import org.openstreetmap.josm.gui.layer.markerlayer.PlayHeadMarker;
@@ -152,13 +151,4 @@
      */
     public void addLayer(Layer layer) {
-        if (layer instanceof OsmDataLayer) {
-            OsmDataLayer editLayer = (OsmDataLayer)layer;
-            editLayer.listenerModified.add(new ModifiedChangedListener(){
-                public void modifiedChanged(boolean value, OsmDataLayer source) {
-                    JOptionPane.getFrameForComponent(Main.parent).setTitle((value?"*":"")
-                            +tr("Java OpenStreetMap Editor"));
-                }
-            });
-        }
         if (layer instanceof MarkerLayer && playHeadMarker == null) {
             playHeadMarker = PlayHeadMarker.create();
@@ -510,4 +500,7 @@
             }
         }
+        if (layer instanceof OsmDataLayer) {
+            refreshTitle((OsmDataLayer)layer);
+        }
 
         /* This only makes the buttons look disabled. Disabling the actions as well requires
@@ -600,4 +593,19 @@
         if (evt.getPropertyName().equals(Layer.VISIBLE_PROP)) {
             repaint();
+        } else if (evt.getPropertyName().equals(OsmDataLayer.REQUIRES_SAVE_TO_DISK_PROP)
+                || evt.getPropertyName().equals(OsmDataLayer.REQUIRES_UPLOAD_TO_SERVER_PROP)) {
+            OsmDataLayer layer = (OsmDataLayer)evt.getSource();
+            if (layer == getEditLayer()) {
+                refreshTitle(layer);
+            }
+        }
+    }
+
+    protected void refreshTitle(OsmDataLayer layer) {
+        boolean dirty = layer.requiresSaveToFile() || layer.requiresUploadToServer();
+        if (dirty) {
+            JOptionPane.getFrameForComponent(Main.parent).setTitle("* " + tr("Java OpenStreetMap Editor"));
+        } else {
+            JOptionPane.getFrameForComponent(Main.parent).setTitle(tr("Java OpenStreetMap Editor"));
         }
     }
Index: trunk/src/org/openstreetmap/josm/gui/NavigatableComponent.java
===================================================================
--- trunk/src/org/openstreetmap/josm/gui/NavigatableComponent.java	(revision 2021)
+++ trunk/src/org/openstreetmap/josm/gui/NavigatableComponent.java	(revision 2025)
@@ -74,5 +74,5 @@
         double dist = getDist100Pixel();
         return dist >= 2000 ? Math.round(dist/100)/10 +" km" : (dist >= 1
-        ? Math.round(dist*10)/10 +" m" : "< 1 m");
+                ? Math.round(dist*10)/10 +" m" : "< 1 m");
     }
 
@@ -112,7 +112,7 @@
                         center.east() - getWidth()/2.0*scale,
                         center.north() - getHeight()/2.0*scale),
-                new EastNorth(
-                        center.east() + getWidth()/2.0*scale,
-                        center.north() + getHeight()/2.0*scale));
+                        new EastNorth(
+                                center.east() + getWidth()/2.0*scale,
+                                center.north() + getHeight()/2.0*scale));
     };
 
@@ -121,5 +121,5 @@
         Bounds b = getProjection().getWorldBoundsLatLon();
         return new ProjectionBounds(getProjection().latlon2eastNorth(b.min),
-            getProjection().latlon2eastNorth(b.max));
+                getProjection().latlon2eastNorth(b.max));
     };
 
@@ -130,7 +130,7 @@
                         center.east() - getWidth()/2.0*scale,
                         center.north() - getHeight()/2.0*scale)),
-                getProjection().eastNorth2latlon(new EastNorth(
-                        center.east() + getWidth()/2.0*scale,
-                        center.north() + getHeight()/2.0*scale)));
+                        getProjection().eastNorth2latlon(new EastNorth(
+                                center.east() + getWidth()/2.0*scale,
+                                center.north() + getHeight()/2.0*scale)));
     };
 
@@ -189,6 +189,7 @@
         if(lon < b.min.lon()) {changed = true; lon = b.min.lon(); }
         else if(lon > b.max.lon()) {changed = true; lon = b.max.lon(); }
-        if(changed)
-          newCenter = new CachedLatLon(lat, lon).getEastNorth();
+        if(changed) {
+            newCenter = new CachedLatLon(lat, lon).getEastNorth();
+        }
         if (!newCenter.equals(center)) {
             EastNorth oldCenter = center;
@@ -211,12 +212,14 @@
             e2 = getProjection().latlon2eastNorth(new LatLon(lat, b.max.lon()));
             d = e2.east() - e1.east();
-            if(d < width*newScale)
+            if(d < width*newScale) {
                 newScale = Math.max(newScaleH, d/width);
+            }
         }
         else
         {
             d = d/(l1.greatCircleDistance(l2)*height*10);
-            if(newScale < d)
+            if(newScale < d) {
                 newScale = d;
+            }
         }
         if (scale != newScale) {
@@ -296,5 +299,5 @@
             return null;
         for (Node n : ds.nodes) {
-            if (n.deleted || n.incomplete) {
+            if (n.isDeleted() || n.incomplete) {
                 continue;
             }
@@ -307,6 +310,6 @@
             // when multiple nodes on one point, prefer new or selected nodes
             else if(dist == minDistanceSq && minPrimitive != null
-                    && ((n.id == 0 && n.isSelected())
-                            || (!minPrimitive.isSelected() && (n.isSelected() || n.id == 0)))) {
+                    && ((n.getId() == 0 && n.isSelected())
+                            || (!minPrimitive.isSelected() && (n.isSelected() || n.getId() == 0)))) {
                 minPrimitive = n;
             }
@@ -327,5 +330,5 @@
             return null;
         for (Way w : ds.ways) {
-            if (w.deleted || w.incomplete) {
+            if (w.isDeleted() || w.incomplete) {
                 continue;
             }
@@ -334,5 +337,5 @@
             for (Node n : w.getNodes()) {
                 i++;
-                if (n.deleted || n.incomplete) {
+                if (n.isDeleted() || n.incomplete) {
                     continue;
                 }
@@ -451,10 +454,10 @@
             return null;
         for (Way w : ds.ways) {
-            if (w.deleted || w.incomplete) {
+            if (w.isDeleted() || w.incomplete) {
                 continue;
             }
             Node lastN = null;
             for (Node n : w.getNodes()) {
-                if (n.deleted || n.incomplete) {
+                if (n.isDeleted() || n.incomplete) {
                     continue;
                 }
@@ -477,5 +480,5 @@
         }
         for (Node n : ds.nodes) {
-            if (!n.deleted && !n.incomplete
+            if (!n.isDeleted() && !n.incomplete
                     && getPoint(n).distanceSq(p) < snapDistance) {
                 nearest.add(n);
@@ -499,5 +502,5 @@
             return null;
         for (Node n : ds.nodes) {
-            if (!n.deleted && !n.incomplete
+            if (!n.isDeleted() && !n.incomplete
                     && getPoint(n).distanceSq(p) < snapDistance) {
                 nearest.add(n);
Index: trunk/src/org/openstreetmap/josm/gui/PleaseWaitRunnable.java
===================================================================
--- trunk/src/org/openstreetmap/josm/gui/PleaseWaitRunnable.java	(revision 2021)
+++ trunk/src/org/openstreetmap/josm/gui/PleaseWaitRunnable.java	(revision 2025)
@@ -45,10 +45,10 @@
      */
     public PleaseWaitRunnable(String title, boolean ignoreException) {
-        this(title, new PleaseWaitProgressMonitor(), ignoreException);
+        this(title, new PleaseWaitProgressMonitor(title), ignoreException);
     }
 
     public PleaseWaitRunnable(String title, ProgressMonitor progressMonitor, boolean ignoreException) {
         this.title = title;
-        this.progressMonitor = progressMonitor == null?new PleaseWaitProgressMonitor():progressMonitor;
+        this.progressMonitor = progressMonitor == null?new PleaseWaitProgressMonitor(title):progressMonitor;
         this.ignoreException = ignoreException;
         this.progressMonitor.addCancelListener(this);
Index: trunk/src/org/openstreetmap/josm/gui/SelectionManager.java
===================================================================
--- trunk/src/org/openstreetmap/josm/gui/SelectionManager.java	(revision 2021)
+++ trunk/src/org/openstreetmap/josm/gui/SelectionManager.java	(revision 2025)
@@ -286,5 +286,5 @@
             // nodes
             for (Node n : nc.getCurrentDataSet().nodes) {
-                if (!n.deleted && !n.incomplete && r.contains(nc.getPoint(n))) {
+                if (!n.isDeleted() && !n.incomplete && r.contains(nc.getPoint(n))) {
                     selection.add(n);
                 }
@@ -293,5 +293,5 @@
             // ways
             for (Way w : nc.getCurrentDataSet().ways) {
-                if (w.deleted || w.getNodesCount() == 0 || w.incomplete) {
+                if (w.isDeleted() || w.getNodesCount() == 0 || w.incomplete) {
                     continue;
                 }
Index: trunk/src/org/openstreetmap/josm/gui/dialogs/LayerListDialog.java
===================================================================
--- trunk/src/org/openstreetmap/josm/gui/dialogs/LayerListDialog.java	(revision 2021)
+++ trunk/src/org/openstreetmap/josm/gui/dialogs/LayerListDialog.java	(revision 2025)
@@ -47,4 +47,5 @@
 import org.openstreetmap.josm.gui.MapView;
 import org.openstreetmap.josm.gui.SideButton;
+import org.openstreetmap.josm.gui.io.SaveLayersDialog;
 import org.openstreetmap.josm.gui.layer.Layer;
 import org.openstreetmap.josm.gui.layer.OsmDataLayer;
@@ -299,113 +300,42 @@
         }
 
-        protected boolean confirmSkipSaving(OsmDataLayer layer) {
-            int result = new ExtendedDialog(Main.parent,
-                    tr("Unsaved Changes"),
-                    tr("There are unsaved changes in the layer''{0}''. Delete the layer anwyay?",layer.getName()),
-                    new String[] {tr("Delete Layer"), tr("Cancel")},
-                    new String[] {"dialogs/delete.png", "cancel.png"}).getValue();
-
-            return result == 1;
-        }
-
-        protected boolean confirmDeleteLayer(Layer layer) {
-            return ConditionalOptionPaneUtil.showConfirmationDialog(
-                    "delete_layer",
-                    Main.parent,
-                    tr("Do you really want to delete layer ''{0}''?", layer.getName()),
-                    tr("Confirmation"),
-                    JOptionPane.YES_NO_OPTION,
-                    JOptionPane.QUESTION_MESSAGE,
-                    JOptionPane.YES_OPTION);
-        }
-
-        protected DeleteDecision confirmDeleteMultipleLayer(Layer layer, int idx, int numLayers) {
-            String options[] = new String[] {
-                    tr("Yes"),
-                    tr("No"),
-                    tr("Delete all"),
-                    tr("Cancel")
-            };
-            int ret = ConditionalOptionPaneUtil.showOptionDialog(
-                    "delete_layer",
-                    Main.parent,
-                    tr("Do you really want to delete layer ''{0}''?", layer.getName()),
-                    tr("Deleting layer {0} of {1}", idx+1, numLayers),
-                    JOptionPane.DEFAULT_OPTION,
-                    JOptionPane.QUESTION_MESSAGE,
-                    options,
-                    options[0]
-            );
-            switch(ret) {
-                case ConditionalOptionPaneUtil.DIALOG_DISABLED_OPTION: return DeleteDecision.deleteAll;
-                case JOptionPane.CLOSED_OPTION: return DeleteDecision.cancel;
-                case 0: return DeleteDecision.deleteCurrent;
-                case 1: return DeleteDecision.dontDeleteCurrent;
-                case 2: return DeleteDecision.deleteAll;
-                case 3: return DeleteDecision.cancel;
-                default:
-                    // shouldn't happen. This is the safest option.
-                    return DeleteDecision.cancel;
-            }
-        }
-
-
-        public void deleteSingleLayer(Layer layer) {
-            if (layer == null)
+        protected boolean enforceUploadOrSaveModifiedData(List<Layer> selectedLayers) {
+            SaveLayersDialog dialog = new SaveLayersDialog(Main.parent);
+            List<OsmDataLayer> layersWithUnmodifiedChanges = new ArrayList<OsmDataLayer>();
+            for (Layer l: selectedLayers) {
+                if (! (l instanceof OsmDataLayer)) {
+                    continue;
+                }
+                OsmDataLayer odl = (OsmDataLayer)l;
+                if (odl.requiresSaveToFile() || odl.requiresUploadToServer()) {
+                    layersWithUnmodifiedChanges.add(odl);
+                }
+            }
+            dialog.prepareForSavingAndUpdatingLayersBeforeDelete();
+            if (!layersWithUnmodifiedChanges.isEmpty()) {
+                dialog.getModel().populate(layersWithUnmodifiedChanges);
+                dialog.setVisible(true);
+                switch(dialog.getUserAction()) {
+                    case CANCEL: return false;
+                    case PROCEED: return true;
+                    default: return false;
+                }
+            }
+            return true;
+        }
+
+        public void actionPerformed(ActionEvent e) {
+            List<Layer> selectedLayers;
+            if (this.layer == null) {
+                selectedLayers = getModel().getSelectedLayers();
+            } else {
+                selectedLayers = Collections.singletonList(this.layer);
+            }
+            if (selectedLayers.isEmpty())
                 return;
-            if (layer instanceof OsmDataLayer) {
-                OsmDataLayer dataLayer = (OsmDataLayer)layer;
-                if (dataLayer.isModified()) {
-                    if (! confirmSkipSaving(dataLayer))
-                        return;
-                }
-                else if (!confirmDeleteLayer(dataLayer))
-                    return;
-            } else {
-                if (!confirmDeleteLayer(layer))
-                    return;
-            }
-            // model and view are going to be updated via LayerChangeListener
-            //
-            Main.main.removeLayer(layer);
-        }
-
-        public void deleteMultipleLayers(List<Layer> layers) {
-            boolean doAskConfirmation = true;
-            for (int i=0; i < layers.size(); i++) {
-                Layer layer = layers.get(i);
-                if (layer instanceof OsmDataLayer) {
-                    OsmDataLayer dataLayer = (OsmDataLayer)layer;
-                    if (dataLayer.isModified() && ! confirmSkipSaving(dataLayer)) {
-                        continue;
-                    }
-                }
-                if (doAskConfirmation) {
-                    DeleteDecision decision = confirmDeleteMultipleLayer(layer, i, layers.size());
-                    switch(decision) {
-                        case deleteCurrent: /* do nothing */ break;
-                        case deleteAll: doAskConfirmation = false; break;
-                        case dontDeleteCurrent: continue;
-                        case cancel: return;
-                    }
-                }
-                // model and view are going to be updated via LayerChangeListener
-                //
-                Main.main.removeLayer(layer);
-            }
-        }
-
-        public void actionPerformed(ActionEvent e) {
-            if (this.layer == null) {
-                List<Layer> selectedLayers = getModel().getSelectedLayers();
-                if (selectedLayers.isEmpty())
-                    return;
-                if (selectedLayers.size() == 1) {
-                    deleteSingleLayer(selectedLayers.get(0));
-                } else {
-                    deleteMultipleLayers(selectedLayers);
-                }
-            } else {
-                deleteSingleLayer(this.layer);
+            if (! enforceUploadOrSaveModifiedData(selectedLayers))
+                return;
+            for(Layer l: selectedLayers) {
+                Main.main.removeLayer(l);
             }
         }
Index: trunk/src/org/openstreetmap/josm/gui/dialogs/RelationListDialog.java
===================================================================
--- trunk/src/org/openstreetmap/josm/gui/dialogs/RelationListDialog.java	(revision 2021)
+++ trunk/src/org/openstreetmap/josm/gui/dialogs/RelationListDialog.java	(revision 2025)
@@ -135,5 +135,5 @@
             int i = 0;
             for (OsmPrimitive e : DataSet.sort(Main.main.getCurrentDataSet().relations)) {
-                if (!e.deleted && !e.incomplete) {
+                if (!e.isDeleted() && !e.incomplete) {
                     list.setElementAt(e, i++);
                 }
@@ -381,6 +381,8 @@
             Relation copy = new Relation();
             copy.cloneFrom(original);
+            // FIXME: id is going to be hidden. How to reset a primitive?
+            //
             copy.id = 0;
-            copy.modified = true;
+            copy.setModified(true);
             RelationEditor editor = RelationEditor.getEditor(
                     Main.main.getEditLayer(),
Index: trunk/src/org/openstreetmap/josm/gui/dialogs/relation/ReferringRelationsBrowser.java
===================================================================
--- trunk/src/org/openstreetmap/josm/gui/dialogs/relation/ReferringRelationsBrowser.java	(revision 2021)
+++ trunk/src/org/openstreetmap/josm/gui/dialogs/relation/ReferringRelationsBrowser.java	(revision 2025)
@@ -107,5 +107,5 @@
                     getLayer(),
                     full,
-                    new PleaseWaitProgressMonitor()
+                    new PleaseWaitProgressMonitor(tr("Loading parent relations"))
             );
             task.setContinuation(
Index: trunk/src/org/openstreetmap/josm/gui/io/AbstractIOTask.java
===================================================================
--- trunk/src/org/openstreetmap/josm/gui/io/AbstractIOTask.java	(revision 2025)
+++ trunk/src/org/openstreetmap/josm/gui/io/AbstractIOTask.java	(revision 2025)
@@ -0,0 +1,96 @@
+// License: GPL. For details, see LICENSE file.
+package org.openstreetmap.josm.gui.io;
+
+/**
+ * This is an abstract task for uploading or saving a data layer.
+ * 
+ */
+public abstract class AbstractIOTask implements Runnable {
+
+    /** indicates whether the task has been cancelled */
+    private boolean cancelled;
+    /** indicates whether the task has been failed */
+    private boolean failed;
+    /** the last exception caught */
+    private Exception lastException;
+
+    public AbstractIOTask() {
+        cancelled = false;
+        failed = false;
+        lastException = null;
+    }
+
+    /**
+     * Replies true if the task has been cancelled
+     * 
+     * @return true if the task has been cancelled
+     */
+    public boolean isCancelled() {
+        return cancelled;
+    }
+
+    /**
+     * Set whether this task has been cancelled
+     * 
+     * @param cancelled true, if the task has been cancelled; false otherwise
+     */
+    protected void setCancelled(boolean cancelled) {
+        this.cancelled = cancelled;
+    }
+
+    /**
+     * Replies true if the task has been failed
+     * 
+     * @return true if the task has been failed
+     */
+    public boolean isFailed() {
+        return failed || lastException != null;
+    }
+
+    /**
+     * Sets whether the task has been failed
+     * 
+     * @param failed whether the task has been failed
+     */
+    protected void setFailed(boolean failed) {
+        this.failed = failed;
+    }
+
+    /**
+     * Replies the last exception caught
+     * 
+     * @return the last exception caught; null, if no exception was caught
+     */
+    public Exception getLastException() {
+        return lastException;
+    }
+
+    /**
+     * Sets the last exception caught
+     * 
+     * @param lastException the last exception
+     */
+    protected void setLastException(Exception lastException) {
+        this.lastException = lastException;
+    }
+
+    /**
+     * Replies true if this  task was successful, i.e. if it wasn't
+     * cancelled and didn't fail
+     * 
+     * @return true if this  task was successful
+     */
+    public boolean isSuccessful() {
+        return !isCancelled() && !isFailed();
+    }
+
+    /**
+     * Runs the task
+     */
+    public abstract void run();
+
+    /**
+     * Cancel the task
+     */
+    public abstract void cancel();
+}
Index: trunk/src/org/openstreetmap/josm/gui/io/FilenameCellEditor.java
===================================================================
--- trunk/src/org/openstreetmap/josm/gui/io/FilenameCellEditor.java	(revision 2025)
+++ trunk/src/org/openstreetmap/josm/gui/io/FilenameCellEditor.java	(revision 2025)
@@ -0,0 +1,155 @@
+// License: GPL. For details, see LICENSE file.
+package org.openstreetmap.josm.gui.io;
+
+import static org.openstreetmap.josm.tools.I18n.tr;
+
+import java.awt.Component;
+import java.awt.GridBagConstraints;
+import java.awt.GridBagLayout;
+import java.awt.event.ActionEvent;
+import java.awt.event.FocusAdapter;
+import java.awt.event.FocusEvent;
+import java.io.File;
+import java.util.EventObject;
+import java.util.concurrent.CopyOnWriteArrayList;
+
+import javax.swing.AbstractAction;
+import javax.swing.JButton;
+import javax.swing.JPanel;
+import javax.swing.JTable;
+import javax.swing.JTextField;
+import javax.swing.event.CellEditorListener;
+import javax.swing.event.ChangeEvent;
+import javax.swing.table.TableCellEditor;
+
+import org.openstreetmap.josm.actions.SaveActionBase;
+
+/**
+ * This is a {@see TableCellEditor} for filenames. It provides a text input field and
+ * a button for launchinig a {@see JFileChooser}.
+ * 
+ *
+ */
+class FilenameCellEditor extends JPanel implements TableCellEditor {
+    private JTextField tfFileName;
+    private CopyOnWriteArrayList<CellEditorListener> listeners;
+    private File value;
+
+    /**
+     * build the GUI
+     */
+    protected void build() {
+        setLayout(new GridBagLayout());
+        GridBagConstraints gc = new GridBagConstraints();
+        gc.gridx = 0;
+        gc.gridy = 0;
+        gc.fill = GridBagConstraints.BOTH;
+        gc.weightx = 1.0;
+        gc.weighty = 1.0;
+        add(tfFileName = new JTextField(), gc);
+
+
+        gc.gridx = 1;
+        gc.gridy = 0;
+        gc.fill = GridBagConstraints.BOTH;
+        gc.weightx = 0.0;
+        gc.weighty = 1.0;
+        add(new JButton(new LaunchFileChooserAction()));
+
+        tfFileName.addFocusListener(
+                new FocusAdapter() {
+                    @Override
+                    public void focusGained(FocusEvent e) {
+                        tfFileName.selectAll();
+                    }
+                }
+        );
+    }
+
+    public FilenameCellEditor() {
+        listeners = new CopyOnWriteArrayList<CellEditorListener>();
+        build();
+    }
+
+
+    public void addCellEditorListener(CellEditorListener l) {
+        if (!listeners.contains(l)) {
+            listeners.add(l);
+        }
+    }
+
+    protected void fireEditingCanceled() {
+        for (CellEditorListener l: listeners) {
+            l.editingCanceled(new ChangeEvent(this));
+        }
+    }
+
+    protected void fireEditingStopped() {
+        for (CellEditorListener l: listeners) {
+            l.editingStopped(new ChangeEvent(this));
+        }
+    }
+
+    public void cancelCellEditing() {
+        fireEditingCanceled();
+    }
+
+    public Object getCellEditorValue() {
+        return value;
+    }
+
+    public boolean isCellEditable(EventObject anEvent) {
+        return true;
+    }
+
+    public void removeCellEditorListener(CellEditorListener l) {
+        if (listeners.contains(l)) {
+            listeners.remove(l);
+        }
+    }
+
+    public boolean shouldSelectCell(EventObject anEvent) {
+        return true;
+    }
+
+    public boolean stopCellEditing() {
+        if (tfFileName.getText() == null || tfFileName.getText().trim().equals("")) {
+            value = null;
+        } else {
+            value = new File(tfFileName.getText());
+        }
+        fireEditingStopped();
+        return true;
+    }
+
+    public void setInitialValue(File initialValue) {
+        this.value = initialValue;
+        if (initialValue == null) {
+            this.tfFileName.setText("");
+        } else {
+            this.tfFileName.setText(initialValue.toString());
+        }
+    }
+
+    public Component getTableCellEditorComponent(JTable table, Object value, boolean isSelected, int row, int column) {
+        SaveLayerInfo info = (SaveLayerInfo)value;
+        setInitialValue(info.getFile());
+        tfFileName.selectAll();
+        return this;
+    }
+
+    class LaunchFileChooserAction extends AbstractAction {
+        public LaunchFileChooserAction() {
+            putValue(NAME, "...");
+            putValue(SHORT_DESCRIPTION, tr("Launch a file chooser to select a file"));
+        }
+
+        public void actionPerformed(ActionEvent e) {
+            File f = SaveActionBase.createAndOpenSaveFileChooser(tr("Select filename"), "osm");
+            if (f != null) {
+                FilenameCellEditor.this.tfFileName.setText(f.toString());
+                FilenameCellEditor.this.tfFileName.selectAll();
+            }
+        }
+    }
+}
Index: trunk/src/org/openstreetmap/josm/gui/io/FlagCellEditor.java
===================================================================
--- trunk/src/org/openstreetmap/josm/gui/io/FlagCellEditor.java	(revision 2025)
+++ trunk/src/org/openstreetmap/josm/gui/io/FlagCellEditor.java	(revision 2025)
@@ -0,0 +1,97 @@
+// License: GPL. For details, see LICENSE file.
+package org.openstreetmap.josm.gui.io;
+
+import java.awt.Component;
+import java.awt.event.MouseAdapter;
+import java.awt.event.MouseEvent;
+import java.util.EventObject;
+import java.util.concurrent.CopyOnWriteArrayList;
+
+import javax.swing.JCheckBox;
+import javax.swing.JTable;
+import javax.swing.event.CellEditorListener;
+import javax.swing.event.ChangeEvent;
+import javax.swing.table.TableCellEditor;
+
+/**
+ * This is {@see TableCellEditor} for a boolean flag. It is used in two table columns of
+ * {@see SaveLayersTable} and renders the values of {@see SaveLayerInfo#isDoSaveToFile()}
+ * and {@see SaveLayerInfo#isDoUploadToServer()}
+ * 
+ */
+class SaveFlagCellEditor extends JCheckBox implements TableCellEditor {
+    private CopyOnWriteArrayList<CellEditorListener> listeners;
+    private boolean value;
+
+    public SaveFlagCellEditor() {
+        listeners = new CopyOnWriteArrayList<CellEditorListener>();
+        addMouseListener(
+                new MouseAdapter() {
+                    @Override
+                    public void mouseExited(MouseEvent e) {
+                        stopCellEditing();
+                    }
+                }
+        );
+    }
+
+    public void addCellEditorListener(CellEditorListener l) {
+        if (!listeners.contains(l)) {
+            listeners.add(l);
+        }
+    }
+
+    protected void fireEditingCanceled() {
+        for (CellEditorListener l: listeners) {
+            l.editingCanceled(new ChangeEvent(this));
+        }
+    }
+
+    protected void fireEditingStopped() {
+        for (CellEditorListener l: listeners) {
+            l.editingStopped(new ChangeEvent(this));
+        }
+    }
+
+    public void cancelCellEditing() {
+        fireEditingCanceled();
+    }
+
+    public Object getCellEditorValue() {
+        return value;
+    }
+
+    public boolean isCellEditable(EventObject anEvent) {
+        return true;
+    }
+
+    public void removeCellEditorListener(CellEditorListener l) {
+        if (listeners.contains(l)) {
+            listeners.remove(l);
+        }
+    }
+
+    public boolean shouldSelectCell(EventObject anEvent) {
+        return true;
+    }
+
+    public void setInitialValue(boolean value) {
+        this.value = value;
+        setSelected(value);
+    }
+
+    public boolean stopCellEditing() {
+        this.value = isSelected();
+        fireEditingStopped();
+        return true;
+    }
+
+    public Component getTableCellEditorComponent(JTable table, Object value, boolean isSelected, int row, int column) {
+        SaveLayerInfo info = (SaveLayerInfo)value;
+        switch(column) {
+            case 4: setInitialValue(info.isDoUploadToServer()); break;
+            case 5: setInitialValue(info.isDoSaveToFile()); break;
+        }
+        return this;
+    }
+}
Index: trunk/src/org/openstreetmap/josm/gui/io/SaveLayerInfo.java
===================================================================
--- trunk/src/org/openstreetmap/josm/gui/io/SaveLayerInfo.java	(revision 2025)
+++ trunk/src/org/openstreetmap/josm/gui/io/SaveLayerInfo.java	(revision 2025)
@@ -0,0 +1,200 @@
+// License: GPL. For details, see LICENSE file.
+package org.openstreetmap.josm.gui.io;
+
+import java.io.File;
+
+import org.openstreetmap.josm.gui.layer.OsmDataLayer;
+import static org.openstreetmap.josm.tools.I18n.tr;
+
+/**
+ * SaveLayerInfo represents the information, user preferences and save/upload states of
+ * a layer which might be uploaded/saved.
+ * 
+ */
+class SaveLayerInfo implements Comparable<SaveLayerInfo> {
+
+    /** the osm data layer */
+    private OsmDataLayer layer;
+    private boolean doSaveToFile;
+    private boolean doUploadToServer;
+    private File file;
+    private UploadOrSaveState uploadState;
+    private UploadOrSaveState saveState;
+
+    /**
+     * 
+     * @param layer the layer. Must not be null.
+     * @throws IllegalArgumentException thrown if layer is null
+     */
+    public SaveLayerInfo(OsmDataLayer layer) {
+        if (layer == null)
+            throw new IllegalArgumentException(tr("parameter ''{0}'' must not be null", "layer"));
+        this.layer = layer;
+        this.doSaveToFile = layer.requiresSaveToFile();
+        this.doUploadToServer = layer.requiresUploadToServer();
+        this.file = layer.getAssociatedFile();
+    }
+
+    /**
+     * Replies the layer this info objects holds information for
+     * 
+     * @return the layer this info objects holds information for
+     */
+    public OsmDataLayer getLayer() {
+        return layer;
+    }
+
+    /**
+     * Replies true if this layer should be saved to a file; false, otherwise
+     * 
+     * @return true if this layers should be saved to a file; false, otherwise
+     */
+    public boolean isDoSaveToFile() {
+        return doSaveToFile;
+    }
+
+    /**
+     * Sets whether this layer should be saved to a file
+     * 
+     * @param doSaveToFile true to save; false, to skip saving
+     */
+    public void setDoSaveToFile(boolean doSaveToFile) {
+        this.doSaveToFile = doSaveToFile;
+    }
+
+    /**
+     * Replies true if this layer should be uploaded to the server; false, otherwise
+     * 
+     * @return true if this layer should be uploaded to the server; false, otherwise
+     */
+    public boolean isDoUploadToServer() {
+        return doUploadToServer;
+    }
+
+    /**
+     * Sets whether this layer should be uploaded to a file
+     * 
+     * @param doSaveToFile true to upload; false, to skip uploading
+     */
+
+    public void setDoUploadToServer(boolean doUploadToServer) {
+        this.doUploadToServer = doUploadToServer;
+    }
+
+    /**
+     * Replies true if this layer should be uploaded to the server and saved to file.
+     * 
+     * @return true if this layer should be uploaded to the server and saved to file
+     */
+    public boolean isDoSaveAndUpload() {
+        return isDoSaveToFile() && isDoUploadToServer();
+    }
+
+    /**
+     * Replies the name of the layer
+     * 
+     * @return the name of the layer
+     */
+    public String getName() {
+        return layer.getName() == null ? "" : layer.getName();
+    }
+
+    /**
+     * Replies the file this layer should be saved to, if {@see #isDoSaveToFile()} is true
+     * 
+     * @return the file this layer should be saved to, if {@see #isDoSaveToFile()} is true
+     */
+    public File getFile() {
+        return file;
+    }
+
+    /**
+     * Sets the file this layer should be saved to, if {@see #isDoSaveToFile()} is true
+     * 
+     * @param file the file
+     */
+    public void setFile(File file) {
+        this.file = file;
+    }
+
+    public int compareTo(SaveLayerInfo o) {
+        if (isDoSaveAndUpload()) {
+            if (o.isDoSaveAndUpload())
+                return getName().compareTo(o.getName());
+            return -1;
+        } else if (o.isDoSaveAndUpload())
+            return 1;
+        if (isDoUploadToServer()) {
+            if (o.isDoUploadToServer())
+                return getName().compareTo(o.getName());
+            return -1;
+        } else if (o.isDoUploadToServer())
+            return 1;
+        if (isDoSaveToFile()) {
+            if (o.isDoSaveToFile())
+                return getName().compareTo(o.getName());
+            return -1;
+        } else if (o.isDoSaveToFile())
+            return 1;
+        return getName().compareTo(o.getName());
+    }
+
+    /**
+     * Replies the upload state of {@see #getLayer()}.
+     * <ul>
+     *   <li>{@see UploadOrSaveState#OK} if {@see #getLayer() was successfully uploaded</li>
+     *   <li>{@see UploadOrSaveState#CANCELLED} if uploading {@see #getLayer() was cancelled</li>
+     *   <li>{@see UploadOrSaveState#FAILED} if uploading {@see #getLayer() has failed</li>
+     * </ul>
+     * 
+     * @return the upload state
+     */
+    public UploadOrSaveState getUploadState() {
+        return uploadState;
+    }
+
+    /**
+     * Sets the upload state for {@see #getLayer()}
+     * 
+     * @param uploadState the upload state
+     */
+    public void setUploadState(UploadOrSaveState uploadState) {
+        this.uploadState = uploadState;
+    }
+
+    /**
+     * Replies the save state of {@see #getLayer()}.
+     * <ul>
+     *   <li>{@see UploadOrSaveState#OK} if {@see #getLayer() was successfully saved to file</li>
+     *   <li>{@see UploadOrSaveState#CANCELLED} if saving {@see #getLayer() was cancelled</li>
+     *   <li>{@see UploadOrSaveState#FAILED} if saving {@see #getLayer() has failed</li>
+     * </ul>
+     * 
+     * @return the save state
+     */
+    public UploadOrSaveState getSaveState() {
+        return saveState;
+    }
+
+    /**
+     * Sets the save state for {@see #getLayer()}
+     * 
+     * @param saveState save the upload state
+     */
+    public void setSaveState(UploadOrSaveState saveState) {
+        this.saveState = saveState;
+    }
+
+    /**
+     * Resets the upload and save state
+     * 
+     * @see #setUploadState(UploadOrSaveState)
+     * @see #setSaveState(UploadOrSaveState)
+     * @see #getUploadState()
+     * @see #getSaveState()
+     */
+    public void resetUploadAndSaveState() {
+        this.uploadState = null;
+        this.saveState = null;
+    }
+}
Index: trunk/src/org/openstreetmap/josm/gui/io/SaveLayerInfoCellRenderer.java
===================================================================
--- trunk/src/org/openstreetmap/josm/gui/io/SaveLayerInfoCellRenderer.java	(revision 2025)
+++ trunk/src/org/openstreetmap/josm/gui/io/SaveLayerInfoCellRenderer.java	(revision 2025)
@@ -0,0 +1,148 @@
+// License: GPL. For details, see LICENSE file.
+package org.openstreetmap.josm.gui.io;
+
+import static org.openstreetmap.josm.tools.I18n.tr;
+
+import java.awt.Color;
+import java.awt.Component;
+import java.awt.Font;
+import java.io.File;
+import java.io.IOException;
+
+import javax.swing.JCheckBox;
+import javax.swing.JLabel;
+import javax.swing.JTable;
+import javax.swing.UIManager;
+import javax.swing.table.TableCellRenderer;
+
+import org.openstreetmap.josm.io.OsmApi;
+
+/**
+ * This is a {@see TableCellRenderer} for rendering the various fields of a
+ * {@see SaveLayerInfo} in the table {@see SaveLayersTable}.
+ * 
+ *
+ */
+class SaveLayerInfoCellRenderer implements TableCellRenderer {
+    private JLabel lblRenderer;
+    private JCheckBox cbRenderer;
+
+    public SaveLayerInfoCellRenderer() {
+        lblRenderer = new JLabel();
+        cbRenderer = new JCheckBox();
+    }
+
+    protected Component prepareLayerNameRenderer(SaveLayerInfo info) {
+        lblRenderer.setIcon(info.getLayer().getIcon());
+        lblRenderer.setText(info.getName());
+        lblRenderer.setToolTipText(info.getLayer().getToolTipText());
+        return lblRenderer;
+    }
+
+    protected Component prepareUploadRequiredRenderer(SaveLayerInfo info) {
+        lblRenderer.setIcon(null);
+        lblRenderer.setHorizontalAlignment(JLabel.CENTER);
+        String text = info.getLayer().requiresUploadToServer() ? tr("Yes") : tr("No");
+        lblRenderer.setText(text);
+        if (info.getLayer().requiresUploadToServer()) {
+            lblRenderer.setToolTipText(tr("Layer ''{0}'' has modifications which should be uploaded to the server.", info.getName()));
+        } else {
+            lblRenderer.setToolTipText(tr("Layer ''{0}'' has no modifications to be uploaded", info.getName()));
+        }
+        return lblRenderer;
+    }
+
+    protected Component prepareSaveToFileRequired(SaveLayerInfo info) {
+        lblRenderer.setIcon(null);
+        lblRenderer.setHorizontalAlignment(JLabel.CENTER);
+        String text = info.getLayer().requiresSaveToFile() ? tr("Yes") : tr("No");
+        lblRenderer.setText(text);
+        if (info.getLayer().requiresSaveToFile()) {
+            lblRenderer.setToolTipText(tr("Layer ''{0}'' has modifications which should be saved to its assoicated file ''{1}''.", info.getName(), info.getFile().toString()));
+        } else {
+            lblRenderer.setToolTipText(tr("Layer ''{0}'' has no modifications to be saved", info.getName()));
+        }
+        return lblRenderer;
+    }
+
+    protected boolean canWrite(File f) {
+        if (f == null) return false;
+        if (f.isDirectory()) return false;
+        if (f.exists() && f.canWrite()) return true;
+        if (!f.exists() && f.getParentFile() != null && f.getParentFile().canWrite())
+            return true;
+        return false;
+    }
+
+    protected Component prepareFileNameRenderer(SaveLayerInfo info) {
+        lblRenderer.setIcon(null);
+        if (info.getFile() == null) {
+            if (!info.isDoSaveToFile()) {
+                lblRenderer.setText(tr("No file associated with this layer"));
+            } else {
+                lblRenderer.setBackground(new Color(255,197,197));
+                lblRenderer.setText(tr("Please select a file"));
+            }
+            lblRenderer.setFont(lblRenderer.getFont().deriveFont(Font.ITALIC));
+            lblRenderer.setToolTipText(tr("Layer ''{0}'' is not backed by a file", info.getName()));
+        } else {
+            String text = info.getFile().getName();
+            String parent = info.getFile().getParent();
+            if (parent != null) {
+                if (parent.length() <= 10) {
+                    text = info.getFile().getPath();
+                } else {
+                    text = parent.substring(0, 10) + "..." + File.separator + text;
+                }
+            }
+            lblRenderer.setText(text);
+            lblRenderer.setToolTipText(info.getFile().getAbsolutePath());
+            if (info.isDoSaveToFile() && !canWrite(info.getFile())) {
+                lblRenderer.setBackground(new Color(255,197,197));
+                lblRenderer.setToolTipText(tr("File ''{0}'' is not writable. Please enter another file name.", info.getFile().getPath()));
+            }
+        }
+        return lblRenderer;
+    }
+
+    protected Component prepareUploadRenderer(SaveLayerInfo info){
+        cbRenderer.setSelected(info.isDoUploadToServer());
+        lblRenderer.setToolTipText(tr("Select to upload layer ''{0}'' to the server ''{1}''", info.getName(), OsmApi.getOsmApi().getBaseUrl()));
+        return cbRenderer;
+    }
+
+    protected Component prepareSaveToFileRenderer(SaveLayerInfo info){
+        cbRenderer.setSelected(info.isDoSaveToFile());
+        lblRenderer.setToolTipText(tr("Select to upload layer ''{0}'' to the server ''{1}''", info.getName(), OsmApi.getOsmApi().getBaseUrl()));
+        return cbRenderer;
+    }
+
+    protected void resetRenderers() {
+        lblRenderer.setOpaque(true);
+        lblRenderer.setBackground(UIManager.getColor("Table.background"));
+        lblRenderer.setIcon(null);
+        lblRenderer.setText("");
+        lblRenderer.setFont(UIManager.getFont("Table.font"));
+        lblRenderer.setHorizontalAlignment(JLabel.LEFT);
+
+        cbRenderer.setSelected(false);
+        cbRenderer.setOpaque(true);
+        cbRenderer.setBackground(UIManager.getColor("Table.background"));
+    }
+
+    public Component getTableCellRendererComponent(JTable table, Object value, boolean isSelected,
+            boolean hasFocus, int row, int column) {
+
+        resetRenderers();
+        SaveLayerInfo info = (SaveLayerInfo)value;
+        switch(column) {
+            case 0: return prepareLayerNameRenderer(info);
+            case 1: return prepareUploadRequiredRenderer(info);
+            case 2: return prepareSaveToFileRequired(info);
+            case 3: return prepareFileNameRenderer(info);
+            case 4: return prepareUploadRenderer(info);
+            case 5: return prepareSaveToFileRenderer(info);
+        }
+        return null;
+    }
+}
Index: trunk/src/org/openstreetmap/josm/gui/io/SaveLayerTask.java
===================================================================
--- trunk/src/org/openstreetmap/josm/gui/io/SaveLayerTask.java	(revision 2025)
+++ trunk/src/org/openstreetmap/josm/gui/io/SaveLayerTask.java	(revision 2025)
@@ -0,0 +1,68 @@
+// License: GPL. For details, see LICENSE file.
+package org.openstreetmap.josm.gui.io;
+
+import static org.openstreetmap.josm.tools.I18n.tr;
+
+import org.openstreetmap.josm.actions.SaveAction;
+import org.openstreetmap.josm.gui.progress.NullProgressMonitor;
+import org.openstreetmap.josm.gui.progress.ProgressMonitor;
+
+/**
+ * SaveLayerTask saves the data managed by an {@see OsmDataLayer} to the
+ * {@see OsmDataLayer#getAssociatedFile()}.
+ * 
+ * <pre>
+ *     ExecutorService executorService = ...
+ *     SaveLayerTask task = new SaveLayerTask(layer, monitor);
+ *     Future<?> taskFuture = executorServce.submit(task)
+ *     try {
+ *        // wait for the task to complete
+ *        taskFuture.get();
+ *     } catch(Exception e) {
+ *        e.printStackTracek();
+ *     }
+ * </pre>
+ */
+class SaveLayerTask extends AbstractIOTask {
+    private SaveLayerInfo layerInfo;
+    private ProgressMonitor parentMonitor;
+
+    /**
+     * 
+     * @param layerInfo information about the layer to be saved to save. Must not be null.
+     * @param monitor the monitor. Set to {@see NullProgressMonitor#INSTANCE} if null
+     * @throws IllegalArgumentException thrown if layer is null
+     */
+    protected SaveLayerTask(SaveLayerInfo layerInfo, ProgressMonitor monitor) {
+        if (layerInfo == null)
+            throw new IllegalArgumentException(tr("parameter ''{0}'' must not be null", "layerInfo"));
+        if (monitor == null) {
+            monitor = NullProgressMonitor.INSTANCE;
+        }
+        this.layerInfo =  layerInfo;
+        this.parentMonitor = monitor;
+    }
+
+    @Override
+    public void run() {
+        try {
+            parentMonitor.subTask(tr("Saving layer to ''{0}'' ...", layerInfo.getFile().toString()));
+            layerInfo.getLayer().setAssociatedFile(layerInfo.getFile());
+            if (!new SaveAction().doSave(layerInfo.getLayer())) {
+                setFailed(true);
+                return;
+            }
+            if (!isCancelled()) {
+                layerInfo.getLayer().onPostSaveToFile();
+            }
+        } catch(Exception e) {
+            e.printStackTrace();
+            setLastException(e);
+        }
+    }
+
+    @Override
+    public void cancel() {
+        setCancelled(true);
+    }
+}
Index: trunk/src/org/openstreetmap/josm/gui/io/SaveLayersColumnModel.java
===================================================================
--- trunk/src/org/openstreetmap/josm/gui/io/SaveLayersColumnModel.java	(revision 2025)
+++ trunk/src/org/openstreetmap/josm/gui/io/SaveLayersColumnModel.java	(revision 2025)
@@ -0,0 +1,75 @@
+// License: GPL. For details, see LICENSE file.
+package org.openstreetmap.josm.gui.io;
+
+import static org.openstreetmap.josm.tools.I18n.tr;
+
+import javax.swing.table.DefaultTableColumnModel;
+import javax.swing.table.TableCellEditor;
+import javax.swing.table.TableCellRenderer;
+import javax.swing.table.TableColumn;
+
+
+class SaveLayersTableColumnModel extends DefaultTableColumnModel {
+    protected void build() {
+        TableColumn col = null;
+        TableCellRenderer renderer = new SaveLayerInfoCellRenderer();
+        TableCellEditor fileNameEditor = new FilenameCellEditor();
+        TableCellEditor saveFlagEditor = new SaveFlagCellEditor();
+
+        // column 0 - Layer
+        col = new TableColumn(0);
+        col.setHeaderValue(tr("Layer"));
+        col.setResizable(true);
+        col.setCellRenderer(renderer);
+        col.setPreferredWidth(100);
+        addColumn(col);
+
+        // column 1 - Upload required
+        col = new TableColumn(1);
+        col.setHeaderValue(tr("Should upload?"));
+        col.setResizable(true);
+        col.setCellRenderer(renderer);
+        col.setPreferredWidth(50);
+        addColumn(col);
+
+        // column 2 - Save to file required
+        col = new TableColumn(2);
+        col.setHeaderValue(tr("Should save?"));
+        col.setResizable(true);
+        col.setCellRenderer(renderer);
+        col.setPreferredWidth(50);
+        addColumn(col);
+
+        // column 3 - filename
+        col = new TableColumn(3);
+        col.setHeaderValue(tr("Filename"));
+        col.setResizable(true);
+        col.setCellRenderer(renderer);
+        col.setCellEditor(fileNameEditor);
+        col.setPreferredWidth(200);
+        addColumn(col);
+
+        // column 4 - Upload
+        col = new TableColumn(4);
+        col.setHeaderValue(tr("Upload"));
+        col.setResizable(true);
+        col.setCellRenderer(renderer);
+        col.setCellEditor(saveFlagEditor);
+        col.setPreferredWidth(30);
+        addColumn(col);
+
+        // column 5 - Save
+        col = new TableColumn(5);
+        col.setHeaderValue(tr("Save"));
+        col.setResizable(true);
+        col.setCellRenderer(renderer);
+        col.setCellEditor(saveFlagEditor);
+        col.setPreferredWidth(30);
+
+        addColumn(col);
+    }
+
+    public SaveLayersTableColumnModel() {
+        build();
+    }
+}
Index: trunk/src/org/openstreetmap/josm/gui/io/SaveLayersDialog.java
===================================================================
--- trunk/src/org/openstreetmap/josm/gui/io/SaveLayersDialog.java	(revision 2025)
+++ trunk/src/org/openstreetmap/josm/gui/io/SaveLayersDialog.java	(revision 2025)
@@ -0,0 +1,522 @@
+// License: GPL. For details, see LICENSE file.
+package org.openstreetmap.josm.gui.io;
+
+import static org.openstreetmap.josm.tools.I18n.tr;
+import static org.openstreetmap.josm.tools.I18n.trn;
+
+import java.awt.BorderLayout;
+import java.awt.Component;
+import java.awt.Dimension;
+import java.awt.FlowLayout;
+import java.awt.GridBagConstraints;
+import java.awt.GridBagLayout;
+import java.awt.event.ActionEvent;
+import java.awt.event.WindowAdapter;
+import java.awt.event.WindowEvent;
+import java.beans.PropertyChangeEvent;
+import java.beans.PropertyChangeListener;
+import java.util.List;
+import java.util.concurrent.CancellationException;
+import java.util.concurrent.ExecutorService;
+import java.util.concurrent.Executors;
+import java.util.concurrent.Future;
+
+import javax.swing.AbstractAction;
+import javax.swing.DefaultListCellRenderer;
+import javax.swing.JDialog;
+import javax.swing.JLabel;
+import javax.swing.JList;
+import javax.swing.JOptionPane;
+import javax.swing.JPanel;
+import javax.swing.JScrollPane;
+
+import org.openstreetmap.josm.Main;
+import org.openstreetmap.josm.actions.UploadAction;
+import org.openstreetmap.josm.gui.ExceptionDialogUtil;
+import org.openstreetmap.josm.gui.SideButton;
+import org.openstreetmap.josm.gui.io.SaveLayersModel.Mode;
+import org.openstreetmap.josm.gui.progress.ProgressMonitor;
+import org.openstreetmap.josm.gui.progress.SwingRenderingProgressMonitor;
+import org.openstreetmap.josm.tools.ImageProvider;
+import org.openstreetmap.josm.tools.WindowGeometry;
+
+public class SaveLayersDialog extends JDialog {
+    static public enum UserAction {
+        /**
+         * save/upload layers was successful, proceed with operation
+         */
+        PROCEED,
+        /**
+         * save/upload of layers was not successful or user cancelled
+         * operation
+         */
+        CANCEL
+    }
+
+    private SaveLayersModel model;
+    private UserAction action = UserAction.CANCEL;
+    private UploadAndSaveProgressRenderer pnlUploadLayers;
+
+    private SaveAndProceedAction saveAndProceedAction;
+    private DiscardAndProceedAction discardAndProceedAction;
+    private CancelAction cancelAction;
+    private SaveAndUploadTask saveAndUploadTask;
+
+    /**
+     * builds the GUI
+     */
+    protected void build() {
+        WindowGeometry geometry = WindowGeometry.centerOnScreen(new Dimension(600,300));
+        geometry.apply(this);
+        getContentPane().setLayout(new BorderLayout());
+
+        model = new SaveLayersModel();
+        SaveLayersTable table;
+        JScrollPane pane = new JScrollPane(table = new SaveLayersTable(model));
+        model.addPropertyChangeListener(table);
+        getContentPane().add(pane, BorderLayout.CENTER);
+        getContentPane().add(buildButtonRow(), BorderLayout.SOUTH);
+        table.getTableHeader().setPreferredSize(new Dimension(table.getTableHeader().getWidth(), 40));
+
+        addWindowListener(new WindowClosingAdapter());
+    }
+
+    /**
+     * builds the button row
+     * 
+     * @return the panel with the button row
+     */
+    protected JPanel buildButtonRow() {
+        JPanel pnl = new JPanel();
+        pnl.setLayout(new FlowLayout(FlowLayout.CENTER));
+
+        saveAndProceedAction = new SaveAndProceedAction();
+        model.addPropertyChangeListener(saveAndProceedAction);
+        pnl.add(new SideButton(saveAndProceedAction));
+
+        discardAndProceedAction = new DiscardAndProceedAction();
+        model.addPropertyChangeListener(discardAndProceedAction);
+        pnl.add(new SideButton(discardAndProceedAction));
+
+        cancelAction = new CancelAction();
+        pnl.add(new SideButton(cancelAction));
+
+        JPanel pnl2 = new JPanel();
+        pnl2.setLayout(new BorderLayout());
+        pnl2.add(pnlUploadLayers = new UploadAndSaveProgressRenderer(), BorderLayout.CENTER);
+        model.addPropertyChangeListener(pnlUploadLayers);
+        pnl2.add(pnl, BorderLayout.SOUTH);
+        return pnl2;
+    }
+
+    public void prepareForSavingAndUpdatingLayersBeforeExit() {
+        setTitle(tr("Unsaved changes - Save/Upload before exiting?"));
+        this.saveAndProceedAction.initForSaveAndExit();
+        this.discardAndProceedAction.initForDiscardAndExit();
+    }
+
+    public void prepareForSavingAndUpdatingLayersBeforeDelete() {
+        setTitle(tr("Unsaved changes - Save/Upload before deleting?"));
+        this.saveAndProceedAction.initForSaveAndDelete();
+        this.discardAndProceedAction.initForDiscardAndDelete();
+    }
+
+    public SaveLayersDialog(Component parent) {
+        super(JOptionPane.getFrameForComponent(parent), true /* modal */);
+        build();
+    }
+
+    public UserAction getUserAction() {
+        return this.action;
+    }
+
+    public SaveLayersModel getModel() {
+        return model;
+    }
+
+    protected void launchSafeAndUploadTask() {
+        ProgressMonitor monitor = new SwingRenderingProgressMonitor(pnlUploadLayers);
+        monitor.beginTask(tr("Uploading and saving modified layers ..."));
+        this.saveAndUploadTask = new SaveAndUploadTask(model, monitor);
+        new Thread(saveAndUploadTask).start();
+    }
+
+    protected void cancelSafeAndUploadTask() {
+        if (this.saveAndUploadTask != null) {
+            this.saveAndUploadTask.cancel();
+        }
+        model.setMode(Mode.EDITING_DATA);
+    }
+
+    private static class  LayerListWarningMessagePanel extends JPanel {
+        private JLabel lblMessage;
+        private JList lstLayers;
+
+        protected void build() {
+            setLayout(new GridBagLayout());
+            GridBagConstraints gc = new GridBagConstraints();
+            gc.gridx = 0;
+            gc.gridy = 0;
+            gc.fill = GridBagConstraints.HORIZONTAL;
+            gc.weightx = 1.0;
+            gc.weighty = 0.0;
+            add(lblMessage = new JLabel(), gc);
+            lblMessage.setHorizontalAlignment(JLabel.LEFT);
+            lstLayers = new JList();
+            lstLayers.setCellRenderer(
+                    new DefaultListCellRenderer() {
+                        @Override
+                        public Component getListCellRendererComponent(JList list, Object value, int index,
+                                boolean isSelected, boolean cellHasFocus) {
+                            SaveLayerInfo info = (SaveLayerInfo)value;
+                            setIcon(info.getLayer().getIcon());
+                            setText(info.getName());
+                            return this;
+                        }
+                    }
+            );
+            gc.gridx = 0;
+            gc.gridy = 1;
+            gc.fill = GridBagConstraints.HORIZONTAL;
+            gc.weightx = 1.0;
+            gc.weighty = 1.0;
+            add(lstLayers,gc);
+        }
+
+        public LayerListWarningMessagePanel(String msg, List<SaveLayerInfo> infos) {
+            build();
+            lblMessage.setText(msg);
+            lstLayers.setListData(infos.toArray());
+        }
+    }
+
+    protected void warnLayersWithConflictsAndUploadRequest(List<SaveLayerInfo> infos) {
+        String msg = trn("<html>{0} layer has unresolved conflicts.<br>"
+                + "Either resolve them first or discard the modifications.<br>"
+                + "Layer with conflicts:</html>",
+                "<html>{0} layers have unresolved conflicts.<br>"
+                + "Either resolve them first or discard the modifications.<br>"
+                + "Layers with conflicts:</html>",
+                infos.size(),
+                infos.size());
+        JOptionPane.showConfirmDialog(
+                Main.parent,
+                new LayerListWarningMessagePanel(msg, infos),
+                tr("Unsafed data and conflicts"),
+                JOptionPane.DEFAULT_OPTION,
+                JOptionPane.WARNING_MESSAGE
+        );
+    }
+
+    protected void warnLayersWithoutFilesAndSaveRequest(List<SaveLayerInfo> infos) {
+        String msg = trn("<html>{0} layer needs saving but has no associated file.<br>"
+                + "Either select a file for this layer or discard the changes.<br>"
+                + "Layer without a file:</html>",
+                "<html>{0} layers need saving but have no associated file.<br>"
+                + "Either select a file for each of them or discard the changes.<br>"
+                + "Layers without a file:</html>",
+                infos.size(),
+                infos.size());
+        JOptionPane.showConfirmDialog(
+                Main.parent,
+                new LayerListWarningMessagePanel(msg, infos),
+                tr("Unsafed data and missing associated file"),
+                JOptionPane.DEFAULT_OPTION,
+                JOptionPane.WARNING_MESSAGE
+        );
+    }
+
+    protected void warnLayersWithIllegalFilesAndSaveRequest(List<SaveLayerInfo> infos) {
+        String msg = trn("<html>{0} layer needs saving but has an associated file<br>"
+                + "which can't be written.<br>"
+                + "Either select another file for this layer or discard the changes.<br>"
+                + "Layer with a non-writable file:</html>",
+                "<html>{0} layers need saving but have associated files<br>"
+                + "which can't be written.<br>"
+                + "Either select another file for each of them or discard the changes.<br>"
+                + "Layers with non-writable files:</html>",
+                infos.size(),
+                infos.size());
+        JOptionPane.showConfirmDialog(
+                Main.parent,
+                new LayerListWarningMessagePanel(msg, infos),
+                tr("Unsafed data non-writable files"),
+                JOptionPane.DEFAULT_OPTION,
+                JOptionPane.WARNING_MESSAGE
+        );
+    }
+
+    protected boolean confirmSaveLayerInfosOK() {
+        List<SaveLayerInfo> layerInfos = model.getLayersWithConflictsAndUploadRequest();
+        if (!layerInfos.isEmpty()) {
+            warnLayersWithConflictsAndUploadRequest(layerInfos);
+            return false;
+        }
+
+        layerInfos = model.getLayersWithoutFilesAndSaveRequest();
+        if (!layerInfos.isEmpty()) {
+            warnLayersWithoutFilesAndSaveRequest(layerInfos);
+            return false;
+        }
+
+        layerInfos = model.getLayersWithIllegalFilesAndSaveRequest();
+        if (!layerInfos.isEmpty()) {
+            warnLayersWithIllegalFilesAndSaveRequest(layerInfos);
+            return false;
+        }
+
+        return true;
+    }
+
+    protected void setUserAction(UserAction action) {
+        this.action = action;
+    }
+
+    class WindowClosingAdapter extends WindowAdapter {
+        @Override
+        public void windowClosing(WindowEvent e) {
+            cancelAction.cancel();
+        }
+    }
+
+    class CancelAction extends AbstractAction {
+        public CancelAction() {
+            putValue(NAME, tr("Cancel"));
+            putValue(SHORT_DESCRIPTION, tr("Close this dialog and resume editing in JOSM"));
+            putValue(SMALL_ICON, ImageProvider.get("cancel"));
+        }
+
+        protected void cancelWhenInEditingModel() {
+            setUserAction(UserAction.CANCEL);
+            setVisible(false);
+        }
+
+        protected void cancelWhenInSaveAndUploadingMode() {
+            cancelSafeAndUploadTask();
+        }
+
+        public void cancel() {
+            switch(model.getMode()) {
+                case EDITING_DATA: cancelWhenInEditingModel(); break;
+                case UPLOADING_AND_SAVING: cancelSafeAndUploadTask(); break;
+            }
+        }
+
+        public void actionPerformed(ActionEvent e) {
+            cancel();
+        }
+    }
+
+    class DiscardAndProceedAction extends AbstractAction  implements PropertyChangeListener {
+        public DiscardAndProceedAction() {
+            initForDiscardAndExit();
+        }
+
+        public void initForDiscardAndExit() {
+            putValue(NAME, tr("Discard and Exit"));
+            putValue(SHORT_DESCRIPTION, tr("Exit JOSM without saving. Unsaved changes are lost."));
+            putValue(SMALL_ICON, ImageProvider.get("exit"));
+        }
+
+        public void initForDiscardAndDelete() {
+            putValue(NAME, tr("Discard and Delete"));
+            putValue(SHORT_DESCRIPTION, tr("Delete layers without saving. Unsaved changes are lost."));
+            putValue(SMALL_ICON, ImageProvider.get("dialogs", "delete"));
+        }
+
+        public void actionPerformed(ActionEvent e) {
+            setUserAction(UserAction.PROCEED);
+            setVisible(false);
+        }
+        public void propertyChange(PropertyChangeEvent evt) {
+            if (evt.getPropertyName().equals(SaveLayersModel.MODE_PROP)) {
+                Mode mode = (Mode)evt.getNewValue();
+                switch(mode) {
+                    case EDITING_DATA: setEnabled(true); break;
+                    case UPLOADING_AND_SAVING: setEnabled(false); break;
+                }
+            }
+        }
+    }
+
+    class SaveAndProceedAction extends AbstractAction implements PropertyChangeListener {
+        public SaveAndProceedAction() {
+            initForSaveAndExit();
+        }
+
+        public void initForSaveAndExit() {
+            putValue(NAME, tr("Save and Exit"));
+            putValue(SHORT_DESCRIPTION, tr("Exit JOSM with saving. Unsaved changes are uploaded and/or saved."));
+            putValue(SMALL_ICON, ImageProvider.get("exit"));
+        }
+
+        public void initForSaveAndDelete() {
+            putValue(NAME, tr("Save and Delete"));
+            putValue(SHORT_DESCRIPTION, tr("Save/Upload layers before deleting. Unsaved changes are not lost."));
+            putValue(SMALL_ICON, ImageProvider.get("dialogs", "delete"));
+        }
+
+        public void actionPerformed(ActionEvent e) {
+            if (! confirmSaveLayerInfosOK())
+                return;
+            launchSafeAndUploadTask();
+        }
+
+        public void propertyChange(PropertyChangeEvent evt) {
+            if (evt.getPropertyName().equals(SaveLayersModel.MODE_PROP)) {
+                SaveLayersModel.Mode mode = (SaveLayersModel.Mode)evt.getNewValue();
+                switch(mode) {
+                    case EDITING_DATA: setEnabled(true); break;
+                    case UPLOADING_AND_SAVING: setEnabled(false); break;
+                }
+            }
+        }
+    }
+
+    /**
+     * This is the asynchronous task which uploads modified layers to the server and
+     * saves them to files, if requested by the user.
+     * 
+     */
+    protected class SaveAndUploadTask implements Runnable {
+
+        private SaveLayersModel model;
+        private ProgressMonitor monitor;
+        private ExecutorService worker;
+        private boolean cancelled;
+        private Future<?> currentFuture;
+        private AbstractIOTask currentTask;
+
+        public SaveAndUploadTask(SaveLayersModel model, ProgressMonitor monitor) {
+            this.model = model;
+            this.monitor = monitor;
+            this.worker = Executors.newSingleThreadExecutor();
+        }
+
+        protected void uploadLayers(List<SaveLayerInfo> toUpload) {
+            for (final SaveLayerInfo layerInfo: toUpload) {
+                if (cancelled) {
+                    model.setUploadState(layerInfo.getLayer(), UploadOrSaveState.CANCELLED);
+                    continue;
+                }
+                monitor.subTask(tr("Preparing layer ''{0}'' for upload ...", layerInfo.getName()));
+
+                if (!new UploadAction().checkPreUploadConditions(layerInfo.getLayer())) {
+                    model.setUploadState(layerInfo.getLayer(), UploadOrSaveState.FAILED);
+                    continue;
+                }
+
+                currentTask = new UploadLayerTask(layerInfo.getLayer(), monitor);
+                currentFuture = worker.submit(currentTask);
+                try {
+                    // wait for the asynchronous task to complete
+                    //
+                    currentFuture.get();
+                } catch(CancellationException e) {
+                    model.setUploadState(layerInfo.getLayer(), UploadOrSaveState.CANCELLED);
+                } catch(Exception e) {
+                    e.printStackTrace();
+                    model.setUploadState(layerInfo.getLayer(), UploadOrSaveState.FAILED);
+                    ExceptionDialogUtil.explainException(e);
+                }
+                if (currentTask.isCancelled()) {
+                    model.setUploadState(layerInfo.getLayer(), UploadOrSaveState.CANCELLED);
+                } else if (currentTask.isFailed()) {
+                    currentTask.getLastException().printStackTrace();
+                    ExceptionDialogUtil.explainException(currentTask.getLastException());
+                    model.setUploadState(layerInfo.getLayer(), UploadOrSaveState.FAILED);
+                } else {
+                    model.setUploadState(layerInfo.getLayer(), UploadOrSaveState.OK);
+                }
+                currentTask = null;
+                currentFuture = null;
+            }
+        }
+
+        protected void saveLayers(List<SaveLayerInfo> toSave) {
+            for (final SaveLayerInfo layerInfo: toSave) {
+                if (cancelled) {
+                    model.setSaveState(layerInfo.getLayer(), UploadOrSaveState.CANCELLED);
+                    continue;
+                }
+                currentTask= new SaveLayerTask(layerInfo, monitor);
+                currentFuture = worker.submit(currentTask);
+
+                try {
+                    // wait for the asynchronous task to complete
+                    //
+                    currentFuture.get();
+                } catch(CancellationException e) {
+                    model.setSaveState(layerInfo.getLayer(), UploadOrSaveState.CANCELLED);
+                } catch(Exception e) {
+                    e.printStackTrace();
+                    model.setSaveState(layerInfo.getLayer(), UploadOrSaveState.FAILED);
+                    ExceptionDialogUtil.explainException(e);
+                }
+                if (currentTask.isCancelled()) {
+                    model.setSaveState(layerInfo.getLayer(), UploadOrSaveState.CANCELLED);
+                } else if (currentTask.isFailed()) {
+                    if (currentTask.getLastException() != null) {
+                        currentTask.getLastException().printStackTrace();
+                        ExceptionDialogUtil.explainException(currentTask.getLastException());
+                    }
+                    model.setSaveState(layerInfo.getLayer(), UploadOrSaveState.FAILED);
+                } else {
+                    model.setSaveState(layerInfo.getLayer(), UploadOrSaveState.OK);
+                }
+                this.currentTask = null;
+                this.currentFuture = null;
+            }
+        }
+
+        protected void warnBecauseOfUnsavedData() {
+            int numProblems = model.getNumCancel() + model.getNumFailed();
+            if (numProblems == 0) return;
+            String msg = tr(
+                    "<html>An upload and/or save operation of one layer with modifications<br>"
+                    + "was cancelled or has been failed.</html>",
+                    "<html>Upload and/or save operations of {0} layers with modifications<br>"
+                    + "were cancelled or have been failed.</html>",
+                    numProblems,
+                    numProblems
+            );
+            JOptionPane.showMessageDialog(
+                    Main.parent,
+                    msg,
+                    tr("Incomplete upload and/or save"),
+                    JOptionPane.WARNING_MESSAGE
+            );
+        }
+
+        public void run() {
+            model.setMode(SaveLayersModel.Mode.UPLOADING_AND_SAVING);
+            List<SaveLayerInfo> toUpload = model.getLayersToUpload();
+            if (!toUpload.isEmpty()) {
+                uploadLayers(toUpload);
+            }
+            List<SaveLayerInfo> toSave = model.getLayersToSave();
+            if (!toSave.isEmpty()) {
+                saveLayers(toSave);
+            }
+            model.setMode(SaveLayersModel.Mode.EDITING_DATA);
+            if (model.hasUnsavedData()) {
+                warnBecauseOfUnsavedData();
+                model.setMode(Mode.EDITING_DATA);
+                if (cancelled) {
+                    setUserAction(UserAction.CANCEL);
+                    setVisible(false);
+                }
+            } else {
+                setUserAction(UserAction.PROCEED);
+                setVisible(false);
+            }
+        }
+
+        public void cancel() {
+            if (currentTask != null) {
+                currentTask.cancel();
+            }
+            cancelled = true;
+        }
+    }
+}
Index: trunk/src/org/openstreetmap/josm/gui/io/SaveLayersModel.java
===================================================================
--- trunk/src/org/openstreetmap/josm/gui/io/SaveLayersModel.java	(revision 2025)
+++ trunk/src/org/openstreetmap/josm/gui/io/SaveLayersModel.java	(revision 2025)
@@ -0,0 +1,219 @@
+// License: GPL. For details, see LICENSE file.
+package org.openstreetmap.josm.gui.io;
+
+import java.beans.PropertyChangeListener;
+import java.beans.PropertyChangeSupport;
+import java.io.File;
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.Comparator;
+import java.util.List;
+
+import javax.swing.table.DefaultTableModel;
+
+import org.openstreetmap.josm.gui.layer.OsmDataLayer;
+
+public class SaveLayersModel extends DefaultTableModel {
+    static final public String MODE_PROP = SaveLayerInfo.class.getName() + ".mode";
+    public enum Mode {
+        EDITING_DATA,
+        UPLOADING_AND_SAVING
+    }
+
+    private List<SaveLayerInfo> layerInfo;
+    private Mode mode;
+    private PropertyChangeSupport support;
+
+    public SaveLayersModel() {
+        mode = Mode.EDITING_DATA;
+        support = new PropertyChangeSupport(this);
+    }
+
+    public void addPropertyChangeListener(PropertyChangeListener l) {
+        support.addPropertyChangeListener(l);
+    }
+
+    public void removePropertyChangeListener(PropertyChangeListener l) {
+        support.removePropertyChangeListener(l);
+    }
+
+    protected void fireModeChanged(Mode oldValue, Mode newValue) {
+        support.firePropertyChange(MODE_PROP, oldValue, newValue);
+    }
+
+    public void setMode(Mode newValue) {
+        Mode oldValue = this.mode;
+        this.mode = newValue;
+        fireModeChanged(oldValue, newValue);
+    }
+
+    public Mode getMode() {
+        return mode;
+    }
+
+    public void populate(List<OsmDataLayer> layers) {
+        layerInfo = new ArrayList<SaveLayerInfo>();
+        if (layers == null) return;
+        for (OsmDataLayer layer: layers) {
+            layerInfo.add(new SaveLayerInfo(layer));
+        }
+        Collections.sort(
+                layerInfo,
+                new Comparator<SaveLayerInfo>() {
+                    public int compare(SaveLayerInfo o1, SaveLayerInfo o2) {
+                        return o1.compareTo(o2);
+                    }
+                }
+        );
+        fireTableDataChanged();
+    }
+
+    @Override
+    public int getRowCount() {
+        if (layerInfo == null) return 0;
+        return layerInfo.size();
+    }
+
+    @Override
+    public Object getValueAt(int row, int column) {
+        if (layerInfo == null) return null;
+        return layerInfo.get(row);
+    }
+
+    @Override
+    public boolean isCellEditable(int row, int column) {
+        return column == 3 || column == 4 || column == 5;
+    }
+
+    @Override
+    public void setValueAt(Object value, int row, int column) {
+        switch(column) {
+            case 3 /* file name */:
+                this.layerInfo.get(row).setFile((File)value);
+                this.layerInfo.get(row).setDoSaveToFile(true);
+                break;
+            case 4 /* upload */: this.layerInfo.get(row).setDoUploadToServer((Boolean)value);break;
+            case 5 /* save */: this.layerInfo.get(row).setDoSaveToFile((Boolean)value);break;
+        }
+        fireTableDataChanged();
+    }
+
+    public List<SaveLayerInfo> getSafeLayerInfo() {
+        return this.layerInfo;
+    }
+
+    public List<SaveLayerInfo> getLayersWithoutFilesAndSaveRequest() {
+        List<SaveLayerInfo> ret =new ArrayList<SaveLayerInfo>();
+        for (SaveLayerInfo info: layerInfo) {
+            if (info.isDoSaveToFile() && info.getFile() == null) {
+                ret.add(info);
+            }
+        }
+        return ret;
+    }
+
+    public List<SaveLayerInfo> getLayersWithIllegalFilesAndSaveRequest() {
+        List<SaveLayerInfo> ret =new ArrayList<SaveLayerInfo>();
+        for (SaveLayerInfo info: layerInfo) {
+            if (info.isDoSaveToFile() && info.getFile() != null && ! info.getFile().canWrite()) {
+                ret.add(info);
+            }
+        }
+        return ret;
+    }
+
+    public List<SaveLayerInfo> getLayersWithConflictsAndUploadRequest() {
+        List<SaveLayerInfo> ret =new ArrayList<SaveLayerInfo>();
+        for (SaveLayerInfo info: layerInfo) {
+            if (info.isDoUploadToServer() && !info.getLayer().getConflicts().isEmpty()) {
+                ret.add(info);
+            }
+        }
+        return ret;
+    }
+
+    public List<SaveLayerInfo> getLayersToUpload() {
+        List<SaveLayerInfo> ret =new ArrayList<SaveLayerInfo>();
+        for (SaveLayerInfo info: layerInfo) {
+            if (info.isDoUploadToServer()) {
+                ret.add(info);
+            }
+        }
+        return ret;
+    }
+
+    public List<SaveLayerInfo> getLayersToSave() {
+        List<SaveLayerInfo> ret =new ArrayList<SaveLayerInfo>();
+        for (SaveLayerInfo info: layerInfo) {
+            if (info.isDoSaveToFile()) {
+                ret.add(info);
+            }
+        }
+        return ret;
+    }
+
+
+    public void setUploadState(OsmDataLayer layer, UploadOrSaveState state) {
+        SaveLayerInfo info = getSaveLayerInfo(layer);
+        if (info != null) {
+            info.setUploadState(state);
+        }
+        fireTableDataChanged();
+    }
+
+    public void setSaveState(OsmDataLayer layer, UploadOrSaveState state) {
+        SaveLayerInfo info = getSaveLayerInfo(layer);
+        if (info != null) {
+            info.setSaveState(state);
+        }
+        fireTableDataChanged();
+    }
+
+    public SaveLayerInfo getSaveLayerInfo(OsmDataLayer layer) {
+        for (SaveLayerInfo info: this.layerInfo) {
+            if (info.getLayer() == layer)
+                return info;
+        }
+        return null;
+    }
+
+    public void resetSaveAndUploadState() {
+        for (SaveLayerInfo info: layerInfo) {
+            info.setSaveState(null);
+            info.setUploadState(null);
+        }
+    }
+
+    public boolean hasUnsavedData() {
+        for (SaveLayerInfo info: layerInfo) {
+            if (info.isDoUploadToServer() && ! UploadOrSaveState.OK.equals(info.getUploadState()))
+                return true;
+            if (info.isDoSaveToFile() && ! UploadOrSaveState.OK.equals(info.getSaveState()))
+                return true;
+        }
+        return false;
+    }
+
+    public int getNumCancel() {
+        int ret = 0;
+        for (SaveLayerInfo info: layerInfo) {
+            if (UploadOrSaveState.CANCELLED.equals(info.getSaveState())
+                    || UploadOrSaveState.CANCELLED.equals(info.getUploadState())) {
+                ret++;
+            }
+        }
+        return ret;
+    }
+
+    public int getNumFailed() {
+        int ret = 0;
+        for (SaveLayerInfo info: layerInfo) {
+            if (UploadOrSaveState.FAILED.equals(info.getSaveState())
+                    || UploadOrSaveState.FAILED.equals(info.getUploadState())) {
+                ret++;
+            }
+        }
+        return ret;
+    }
+
+}
Index: trunk/src/org/openstreetmap/josm/gui/io/SaveLayersTable.java
===================================================================
--- trunk/src/org/openstreetmap/josm/gui/io/SaveLayersTable.java	(revision 2025)
+++ trunk/src/org/openstreetmap/josm/gui/io/SaveLayersTable.java	(revision 2025)
@@ -0,0 +1,26 @@
+// License: GPL. For details, see LICENSE file.
+package org.openstreetmap.josm.gui.io;
+
+import java.beans.PropertyChangeEvent;
+import java.beans.PropertyChangeListener;
+
+import javax.swing.JTable;
+
+import org.openstreetmap.josm.gui.io.SaveLayersModel.Mode;
+
+class SaveLayersTable extends JTable implements PropertyChangeListener {
+    public SaveLayersTable(SaveLayersModel model) {
+        super(model, new SaveLayersTableColumnModel());
+        putClientProperty("terminateEditOnFocusLost", Boolean.TRUE);
+    }
+
+    public void propertyChange(PropertyChangeEvent evt) {
+        if (evt.getPropertyName().equals(SaveLayersModel.MODE_PROP)) {
+            Mode mode = (Mode)evt.getNewValue();
+            switch(mode) {
+                case EDITING_DATA: setEnabled(true);
+                case UPLOADING_AND_SAVING: setEnabled(false);
+            }
+        }
+    }
+}
Index: trunk/src/org/openstreetmap/josm/gui/io/UploadAndSaveProgressRenderer.java
===================================================================
--- trunk/src/org/openstreetmap/josm/gui/io/UploadAndSaveProgressRenderer.java	(revision 2025)
+++ trunk/src/org/openstreetmap/josm/gui/io/UploadAndSaveProgressRenderer.java	(revision 2025)
@@ -0,0 +1,92 @@
+// License: GPL. For details, see LICENSE file.
+package org.openstreetmap.josm.gui.io;
+
+import java.awt.GridBagConstraints;
+import java.awt.GridBagLayout;
+import java.awt.Insets;
+import java.beans.PropertyChangeEvent;
+import java.beans.PropertyChangeListener;
+
+import javax.swing.JLabel;
+import javax.swing.JPanel;
+import javax.swing.JProgressBar;
+
+import org.openstreetmap.josm.gui.io.SaveLayersModel.Mode;
+import org.openstreetmap.josm.gui.progress.ProgressRenderer;
+
+class UploadAndSaveProgressRenderer extends JPanel implements ProgressRenderer, PropertyChangeListener {
+
+    private JLabel lblTaskTitle;
+    private JLabel lblCustomText;
+    private JProgressBar progressBar;
+
+    protected void build() {
+        setLayout(new GridBagLayout());
+        GridBagConstraints gc = new GridBagConstraints();
+        gc.gridx = 0;
+        gc.gridy = 0;
+        gc.fill = GridBagConstraints.HORIZONTAL;
+        gc.weightx = 1.0;
+        gc.weighty = 0.0;
+        gc.insets = new Insets(5,0,0,5);
+        add(lblTaskTitle = new JLabel(""), gc);
+
+        gc.gridx = 0;
+        gc.gridy = 1;
+        gc.fill = GridBagConstraints.HORIZONTAL;
+        gc.weightx = 1.0;
+        gc.weighty = 0.0;
+        gc.insets = new Insets(5,0,0,5);
+        add(lblCustomText = new JLabel(""), gc);
+
+        gc.gridx = 0;
+        gc.gridy = 2;
+        gc.fill = GridBagConstraints.HORIZONTAL;
+        gc.weightx = 1.0;
+        gc.weighty = 0.0;
+        gc.insets = new Insets(5,0,0,5);
+        add(progressBar = new JProgressBar(JProgressBar.HORIZONTAL), gc);
+    }
+
+    public UploadAndSaveProgressRenderer() {
+        build();
+        // initially not visible
+        setVisible(false);
+    }
+
+    public void setCustomText(String message) {
+        lblCustomText.setText(message);
+        repaint();
+    }
+
+    public void setIndeterminate(boolean indeterminate) {
+        progressBar.setIndeterminate(indeterminate);
+        repaint();
+    }
+
+    public void setMaximum(int maximum) {
+        progressBar.setMaximum(maximum);
+        repaint();
+    }
+
+    public void setTaskTitle(String taskTitle) {
+        lblTaskTitle.setText(taskTitle);
+        repaint();
+    }
+
+    public void setValue(int value) {
+        progressBar.setValue(value);
+        repaint();
+    }
+
+    public void propertyChange(PropertyChangeEvent evt) {
+        if (evt.getPropertyName().equals(SaveLayersModel.MODE_PROP)) {
+            Mode mode = (Mode)evt.getNewValue();
+            switch(mode) {
+                case EDITING_DATA: setVisible(false); break;
+                case UPLOADING_AND_SAVING: setVisible(true); break;
+            }
+        }
+        getParent().validate();
+    }
+}
Index: trunk/src/org/openstreetmap/josm/gui/io/UploadLayerTask.java
===================================================================
--- trunk/src/org/openstreetmap/josm/gui/io/UploadLayerTask.java	(revision 2025)
+++ trunk/src/org/openstreetmap/josm/gui/io/UploadLayerTask.java	(revision 2025)
@@ -0,0 +1,88 @@
+// License: GPL. For details, see LICENSE file.
+package org.openstreetmap.josm.gui.io;
+
+import static org.openstreetmap.josm.tools.I18n.tr;
+
+import java.util.Collection;
+
+import org.openstreetmap.josm.data.APIDataSet;
+import org.openstreetmap.josm.data.osm.DataSet;
+import org.openstreetmap.josm.data.osm.OsmPrimitive;
+import org.openstreetmap.josm.gui.layer.OsmDataLayer;
+import org.openstreetmap.josm.gui.progress.NullProgressMonitor;
+import org.openstreetmap.josm.gui.progress.ProgressMonitor;
+import org.openstreetmap.josm.io.OsmServerWriter;
+
+/**
+ * UploadLayerTask uploads the data managed by an {@see OsmDataLayer} asynchronously.
+ * 
+ * <pre>
+ *     ExecutorService executorService = ...
+ *     UploadLayerTask task = new UploadLayerTask(layer, monitor);
+ *     Future<?> taskFuture = executorServce.submit(task)
+ *     try {
+ *        // wait for the task to complete
+ *        taskFuture.get();
+ *     } catch(Exception e) {
+ *        e.printStackTracek();
+ *     }
+ * </pre>
+ */
+class UploadLayerTask extends AbstractIOTask implements Runnable {
+    private OsmServerWriter writer;
+    private OsmDataLayer layer;
+    private ProgressMonitor monitor;
+
+    /**
+     * 
+     * @param layer the layer. Must not be null.
+     * @param monitor  a progress monitor. If monitor is null, uses {@see NullProgressMonitor#INSTANCE}
+     * @throws IllegalArgumentException thrown, if layer is null
+     */
+    public UploadLayerTask(OsmDataLayer layer, ProgressMonitor monitor) {
+        if (layer == null)
+            throw new IllegalArgumentException(tr("parameter ''{0}'' must not be null", layer));
+        if (monitor == null) {
+            monitor = NullProgressMonitor.INSTANCE;
+        }
+        this.layer = layer;
+        this.monitor = monitor;
+    }
+
+    @Override
+    public void run() {
+        monitor.subTask(tr("Preparing primitives to upload ..."));
+        Collection<OsmPrimitive> toUpload = new APIDataSet(layer.data).getPrimitives();
+        if (toUpload.isEmpty())
+            return;
+        writer = new OsmServerWriter();
+        try {
+            ProgressMonitor m = monitor.createSubTaskMonitor(ProgressMonitor.ALL_TICKS, false);
+            if (isCancelled()) return;
+            writer.uploadOsm(layer.data.version, toUpload, m);
+        } catch (Exception sxe) {
+            if (isCancelled()) {
+                System.out.println("Ignoring exception caught because upload is cancelled. Exception is: " + sxe.toString());
+                return;
+            }
+            setLastException(sxe);
+        }
+
+        if (isCancelled())
+            return;
+        layer.cleanupAfterUpload(writer.getProcessedPrimitives());
+        DataSet.fireSelectionChanged(layer.data.getSelected());
+        layer.fireDataChange();
+        layer.onPostUploadToServer();
+    }
+
+    @Override
+    public void cancel() {
+        // make sure the the softCancel operation is serialized with
+        // blocks which can be interrupted.
+        setCancelled(true);
+        if (writer != null) {
+            writer.disconnectActiveConnection();
+        }
+    }
+}
Index: trunk/src/org/openstreetmap/josm/gui/io/UploadOrSaveState.java
===================================================================
--- trunk/src/org/openstreetmap/josm/gui/io/UploadOrSaveState.java	(revision 2025)
+++ trunk/src/org/openstreetmap/josm/gui/io/UploadOrSaveState.java	(revision 2025)
@@ -0,0 +1,17 @@
+// License: GPL. For details, see LICENSE file.
+package org.openstreetmap.josm.gui.io;
+
+public enum UploadOrSaveState {
+    /**
+     * a data layer was successfully saved or upload to the server
+     */
+    OK,
+    /**
+     * uploading or saving a data layer has failed
+     */
+    FAILED,
+    /**
+     * uploading or saving a data layer was cancelled
+     */
+    CANCELLED
+}
Index: trunk/src/org/openstreetmap/josm/gui/layer/GpxLayer.java
===================================================================
--- trunk/src/org/openstreetmap/josm/gui/layer/GpxLayer.java	(revision 2021)
+++ trunk/src/org/openstreetmap/josm/gui/layer/GpxLayer.java	(revision 2025)
@@ -895,5 +895,5 @@
                 }
             }
-            new DownloadOsmTaskList().download(false, toDownload, new PleaseWaitProgressMonitor());
+            new DownloadOsmTaskList().download(false, toDownload, new PleaseWaitProgressMonitor(tr("Download data")));
         }
     }
Index: trunk/src/org/openstreetmap/josm/gui/layer/Layer.java
===================================================================
--- trunk/src/org/openstreetmap/josm/gui/layer/Layer.java	(revision 2021)
+++ trunk/src/org/openstreetmap/josm/gui/layer/Layer.java	(revision 2025)
@@ -45,5 +45,5 @@
 
     /** keeps track of property change listeners */
-    private PropertyChangeSupport propertyChangeSupport;
+    protected PropertyChangeSupport propertyChangeSupport;
 
     /**
Index: trunk/src/org/openstreetmap/josm/gui/layer/OsmDataLayer.java
===================================================================
--- trunk/src/org/openstreetmap/josm/gui/layer/OsmDataLayer.java	(revision 2021)
+++ trunk/src/org/openstreetmap/josm/gui/layer/OsmDataLayer.java	(revision 2025)
@@ -71,4 +71,25 @@
  */
 public class OsmDataLayer extends Layer {
+    static public final String REQUIRES_SAVE_TO_DISK_PROP = OsmDataLayer.class.getName() + ".requiresSaveToDisk";
+    static public final String REQUIRES_UPLOAD_TO_SERVER_PROP = OsmDataLayer.class.getName() + ".requiresUploadToServer";
+
+    private boolean requiresSaveToFile = false;
+    private boolean requiresUploadToServer = false;
+
+    protected void setRequiresSaveToFile(boolean newValue) {
+        boolean oldValue = requiresSaveToFile;
+        requiresSaveToFile = newValue;
+        if (oldValue != newValue) {
+            propertyChangeSupport.firePropertyChange(REQUIRES_SAVE_TO_DISK_PROP, oldValue, newValue);
+        }
+    }
+
+    protected void setRequiresUploadToServer(boolean newValue) {
+        boolean oldValue = requiresUploadToServer;
+        requiresUploadToServer = newValue;
+        if (oldValue != newValue) {
+            propertyChangeSupport.firePropertyChange(REQUIRES_UPLOAD_TO_SERVER_PROP, oldValue, newValue);
+        }
+    }
 
     /** the global counter for created data layers */
@@ -96,5 +117,5 @@
         private void inc(final OsmPrimitive osm, final int i) {
             normal[i]++;
-            if (osm.deleted) {
+            if (osm.isDeleted()) {
                 deleted[i]++;
             }
@@ -113,7 +134,4 @@
     }
 
-    public interface ModifiedChangedListener {
-        void modifiedChanged(boolean value, OsmDataLayer source);
-    }
     public interface CommandQueueListener {
         void commandChanged(int queueSize, int redoSize);
@@ -130,14 +148,4 @@
     private ConflictCollection conflicts;
 
-    /**
-     * Whether the data of this layer was modified during the session.
-     */
-    private boolean modified = false;
-    /**
-     * Whether the data was modified due an upload of the data to the server.
-     */
-    public boolean uploadedModified = false;
-
-    public final LinkedList<ModifiedChangedListener> listenerModified = new LinkedList<ModifiedChangedListener>();
     public final LinkedList<DataChangeListener> listenerDataChanged = new LinkedList<DataChangeListener>();
 
@@ -310,16 +318,7 @@
     @Override public void visitBoundingBox(final BoundingXYVisitor v) {
         for (final Node n : data.nodes)
-            if (!n.deleted && !n.incomplete) {
+            if (!n.isDeleted() && !n.incomplete) {
                 v.visit(n);
             }
-    }
-
-    /**
-     * Cleanup the layer after save to disk. Just marks the layer as unmodified.
-     * Leaves the undo/redo stack unchanged.
-     *
-     */
-    public void cleanupAfterSaveToDisk() {
-        setModified(false);
     }
 
@@ -333,5 +332,4 @@
      */
     public void cleanupAfterUpload(final Collection<OsmPrimitive> processed) {
-
         // return immediately if an upload attempt failed
         if (processed == null || processed.isEmpty())
@@ -351,6 +349,4 @@
             cleanIterator(it, processedSet);
         }
-
-        setModified(true);
     }
 
@@ -367,20 +363,7 @@
         if (!processed.remove(osm))
             return;
-        osm.modified = false;
-        if (osm.deleted) {
+        osm.setModified(false);
+        if (osm.isDeleted()) {
             it.remove();
-        }
-    }
-
-    public boolean isModified() {
-        return modified;
-    }
-
-    public void setModified(final boolean modified) {
-        if (modified == this.modified)
-            return;
-        this.modified = modified;
-        for (final ModifiedChangedListener l : listenerModified) {
-            l.modifiedChanged(modified, this);
         }
     }
@@ -392,5 +375,5 @@
         int size = 0;
         for (final OsmPrimitive osm : list)
-            if (!osm.deleted) {
+            if (!osm.isDeleted()) {
                 size++;
             }
@@ -446,4 +429,6 @@
 
     public void fireDataChange() {
+        setRequiresSaveToFile(true);
+        setRequiresUploadToServer(true);
         for (DataChangeListener dcl : listenerDataChanged) {
             dcl.dataChanged(this);
@@ -456,5 +441,5 @@
         HashSet<Node> doneNodes = new HashSet<Node>();
         for (Way w : data.ways) {
-            if (w.incomplete || w.deleted) {
+            if (w.incomplete || w.isDeleted()) {
                 continue;
             }
@@ -468,5 +453,5 @@
             ArrayList<WayPoint> trkseg = null;
             for (Node n : w.getNodes()) {
-                if (n.incomplete || n.deleted) {
+                if (n.incomplete || n.isDeleted()) {
                     trkseg = null;
                     continue;
@@ -492,5 +477,5 @@
         // records them?
         for (Node n : data.nodes) {
-            if (n.incomplete || n.deleted || doneNodes.contains(n)) {
+            if (n.incomplete || n.isDeleted() || doneNodes.contains(n)) {
                 continue;
             }
@@ -546,3 +531,55 @@
         return conflicts;
     }
+
+    /**
+     * Replies true if the data managed by this layer needs to be uploaded to
+     * the server because it contains at least one modified primitive.
+     * 
+     * @return true if the data managed by this layer needs to be uploaded to
+     * the server because it contains at least one modified primitive; false,
+     * otherwise
+     */
+    public boolean requiresUploadToServer() {
+        return requiresUploadToServer;
+    }
+
+    /**
+     * Replies true if the data managed by this layer needs to be saved to
+     * a file. Only replies true if a file is assigned to this layer and
+     * if the data managed by this layer has been modified since the last
+     * save operation to the file.
+     * 
+     * @return true if the data managed by this layer needs to be saved to
+     * a file
+     */
+    public boolean requiresSaveToFile() {
+        return getAssociatedFile() != null && requiresSaveToFile;
+    }
+
+    /**
+     * Initializes the layer after a successful load of OSM data from a file
+     * 
+     */
+    public void onPostLoadFromFile() {
+        setRequiresSaveToFile(false);
+        setRequiresUploadToServer(data.isModified());
+    }
+
+    /**
+     * Initializes the layer after a successful save of OSM data to a file
+     * 
+     */
+    public void onPostSaveToFile() {
+        setRequiresSaveToFile(false);
+        setRequiresUploadToServer(data.isModified());
+    }
+
+    /**
+     * Initializes the layer after a successful upload to the server
+     * 
+     */
+    public void onPostUploadToServer() {
+        setRequiresUploadToServer(false);
+        // keep requiresSaveToDisk unchanged
+    }
 }
Index: trunk/src/org/openstreetmap/josm/gui/progress/PleaseWaitProgressMonitor.java
===================================================================
--- trunk/src/org/openstreetmap/josm/gui/progress/PleaseWaitProgressMonitor.java	(revision 2021)
+++ trunk/src/org/openstreetmap/josm/gui/progress/PleaseWaitProgressMonitor.java	(revision 2025)
@@ -28,7 +28,9 @@
 
     private PleaseWaitDialog dialog;
+    private String windowTitle;
 
-    public PleaseWaitProgressMonitor() {
+    public PleaseWaitProgressMonitor(String windowTitle) {
         this(JOptionPane.getFrameForComponent(Main.map));
+        this.windowTitle = windowTitle;
     }
 
@@ -86,4 +88,7 @@
                     throw new ProgressException("PleaseWaitDialog parent must be either Frame or Dialog");
 
+                if (windowTitle != null) {
+                    dialog.setTitle(windowTitle);
+                }
                 dialog.cancel.setEnabled(true);
                 dialog.setCustomText("");
Index: trunk/src/org/openstreetmap/josm/gui/progress/ProgressRenderer.java
===================================================================
--- trunk/src/org/openstreetmap/josm/gui/progress/ProgressRenderer.java	(revision 2025)
+++ trunk/src/org/openstreetmap/josm/gui/progress/ProgressRenderer.java	(revision 2025)
@@ -0,0 +1,15 @@
+// License: GPL. For details, see LICENSE file.
+package org.openstreetmap.josm.gui.progress;
+
+/**
+ * Swing components can implement this interface and use a {@see SwingRenderingProgressMonitor}
+ * to render progress information.
+ * 
+ */
+public interface ProgressRenderer {
+    void setTaskTitle(String taskTitle);
+    void setCustomText(String message);
+    void setIndeterminate(boolean indeterminate);
+    void setMaximum(int maximum);
+    void setValue(int value);
+}
Index: trunk/src/org/openstreetmap/josm/gui/progress/SwingRenderingProgressMonitor.java
===================================================================
--- trunk/src/org/openstreetmap/josm/gui/progress/SwingRenderingProgressMonitor.java	(revision 2025)
+++ trunk/src/org/openstreetmap/josm/gui/progress/SwingRenderingProgressMonitor.java	(revision 2025)
@@ -0,0 +1,100 @@
+// License: GPL. For details, see LICENSE file.
+package org.openstreetmap.josm.gui.progress;
+
+import javax.swing.SwingUtilities;
+import static org.openstreetmap.josm.tools.I18n.tr;
+
+/**
+ * SwingRenderingProgressMonitor is progress monitor which delegates the rendering
+ * of progress information to a {@see ProgressRenderer}.
+ * Methods of the progress renderer are always called on the Swing EDT.
+ *
+ */
+public class SwingRenderingProgressMonitor extends AbstractProgressMonitor {
+    private static final int PROGRESS_BAR_MAX = 100;
+    private int currentProgressValue = 0;
+    private ProgressRenderer delegate;
+
+    /**
+     * 
+     * @param delegate the delegate which renders the progress information. Must not be null.
+     * @throws IllegalArgumentException thrown if delegate is null
+     * 
+     */
+    public SwingRenderingProgressMonitor(ProgressRenderer delegate) {
+        super(new CancelHandler());
+        if (delegate == null)
+            throw new IllegalArgumentException(tr("parameter ''{0}'' must not be null", "delegate"));
+        this.delegate = delegate;
+    }
+
+    private void doInEDT(Runnable runnable) {
+        if (SwingUtilities.isEventDispatchThread()) {
+            runnable.run();
+        } else {
+            SwingUtilities.invokeLater(runnable);
+        }
+    }
+
+    @Override
+    public void doBeginTask() {
+        doInEDT(new Runnable() {
+            public void run() {
+                delegate.setCustomText("");
+                delegate.setMaximum(PROGRESS_BAR_MAX);
+            }
+        });
+    }
+
+    @Override
+    public void doFinishTask() {
+        // do nothing
+    }
+
+    @Override
+    protected void updateProgress(double progressValue) {
+        final int newValue = (int)(progressValue * PROGRESS_BAR_MAX);
+        if (newValue != currentProgressValue) {
+            currentProgressValue = newValue;
+            doInEDT(new Runnable() {
+                public void run() {
+                    delegate.setValue(currentProgressValue);
+                }
+            });
+        }
+    }
+
+    @Override
+    protected void doSetCustomText(final String title) {
+        checkState(State.IN_TASK, State.IN_SUBTASK);
+        doInEDT(new Runnable() {
+            public void run() {
+                delegate.setCustomText(title);
+            }
+        });
+    }
+
+    @Override
+    protected void doSetTitle(final String title) {
+        checkState(State.IN_TASK, State.IN_SUBTASK);
+        doInEDT(new Runnable() {
+            public void run() {
+                delegate.setTaskTitle(title);
+            }
+        });
+    }
+
+    @Override
+    protected void doSetIntermediate(final boolean value) {
+        doInEDT(new Runnable() {
+            public void run() {
+                delegate.setIndeterminate(value);
+            }
+        });
+    }
+
+    @Override
+    protected void doSetErrorMessage(String message) {
+        // Do nothing
+    }
+}
